[CalendarServer-changes] [8994] CalendarServer/branches/users/gaya/ldapdirectorybacker/twistedcaldav /directory
source_changes at macosforge.org
source_changes at macosforge.org
Fri Apr 6 12:47:21 PDT 2012
Revision: 8994
http://trac.macosforge.org/projects/calendarserver/changeset/8994
Author: gaya at apple.com
Date: 2012-04-06 12:47:21 -0700 (Fri, 06 Apr 2012)
Log Message:
-----------
ignore system records by ds uid (as well as guid)
Added Paths:
-----------
CalendarServer/branches/users/gaya/ldapdirectorybacker/twistedcaldav/directory/opendirectorybacker.py
Removed Paths:
-------------
CalendarServer/branches/users/gaya/ldapdirectorybacker/twistedcaldav/directory/opendirectorybacker.py
Deleted: CalendarServer/branches/users/gaya/ldapdirectorybacker/twistedcaldav/directory/opendirectorybacker.py
===================================================================
--- CalendarServer/branches/users/gaya/ldapdirectorybacker/twistedcaldav/directory/opendirectorybacker.py 2012-04-06 18:26:59 UTC (rev 8993)
+++ CalendarServer/branches/users/gaya/ldapdirectorybacker/twistedcaldav/directory/opendirectorybacker.py 2012-04-06 19:47:21 UTC (rev 8994)
@@ -1,1730 +0,0 @@
-##
-# Copyright (c) 2006-2012 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-
-"""
-Apple Open Directory directory service implementation for backing up directory-backed address books
-"""
-
-__all__ = [
- "OpenDirectoryBackingService", "ABDirectoryQueryResult",
-]
-
-import traceback
-import hashlib
-
-import os
-import sys
-import time
-
-from os.path import join
-from random import random
-
-from pycalendar.n import N
-from pycalendar.adr import Adr
-from pycalendar.datetime import PyCalendarDateTime
-
-from socket import getfqdn
-
-from twisted.internet.defer import inlineCallbacks, returnValue, deferredGenerator, succeed
-from txdav.xml import element as davxml
-from txdav.xml.base import twisted_dav_namespace, dav_namespace, parse_date, twisted_private_namespace
-from twext.web2.dav.resource import DAVPropertyMixIn
-from twext.web2.dav.util import joinURL
-from twext.web2.http_headers import MimeType, generateContentType, ETag
-
-
-from twistedcaldav import carddavxml
-from twistedcaldav.config import config
-from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
-from twistedcaldav.query import addressbookqueryfilter
-from twistedcaldav.vcard import Component, Property, vCardProductID
-
-from xmlrpclib import datetime
-
-from calendarserver.platform.darwin.od import dsattributes, dsquery
-from twisted.python.reflect import namedModule
-
-from twext.python.log import LoggingMixIn
-
-class OpenDirectoryBackingService(DirectoryService):
- """
- Open Directory implementation of L{IDirectoryService}.
- """
-
- baseGUID = "BF07A1A2-5BB5-4A4D-A59A-67260EA7E143"
-
- def __repr__(self):
- return "<%s %r>" % (self.__class__.__name__, self.realmName, )
-
- def __init__(self, params):
- self._actuallyConfigure(**params)
-
- def _actuallyConfigure(
- self, queryPeopleRecords=True,
- peopleNode = "/Search/Contacts",
- queryUserRecords=True,
- userNode = "/Search",
- maxDSQueryRecords = 0, # maximum number of records requested for any ds query
-
- queryDSLocal = False, #query in DSLocal -- debug
- dsLocalCacheTimeout = 30,
- ignoreSystemRecords = True,
-
- fakeETag = True, # eTag is not reliable if True
-
- addDSAttrXProperties=False, # add dsattributes to vcards as "X-" attributes
- generateSimpleUIDs = False, # use simple synthetic UIDs --- good for testing
- appleInternalServer=False,
-
- additionalAttributes=[],
- allowedAttributes=[],
- directoryBackedAddressBook=None
- ):
- """
- @queryPeopleRecords: C{True} to query for People records
- @queryUserRecords: C{True} to query for User records
- @maxDSQueryRecords: maximum number of (unfiltered) ds records retrieved before raising
- NumberOfMatchesWithinLimits exception or returning results
- @dsLocalCacheTimeout: how log to keep cache of DSLocal records
- @fakeETag: C{True} to use a fake eTag; allows ds queries with partial attributes
- @generateSimpleUIDs: C{True} when creating synthetic UID (==f(Node, Type, Record Name)),
- use a standard Node name. This allows testing with the same UID on different hosts
- @allowedAttributes: list of DSAttributes that are used to create VCards
-
- """
- assert directoryBackedAddressBook is not None
- self.directoryBackedAddressBook = directoryBackedAddressBook
-
- self.peopleDirectory = None
- self.peopleNode = None
- self.userDirectory = None
- self.userNode = None
-
- self.realmName = None # needed for super
-
- self.odModule = namedModule(config.OpenDirectoryModule)
-
- if queryPeopleRecords or not queryUserRecords:
- self.peopleNode = peopleNode
- try:
- self.peopleDirectory = self.odModule.odInit(peopleNode)
- except self.odModule.ODError, e:
- self.log_error("Open Directory (node=%s) Initialization error: %s" % (peopleNode, e))
- raise
- self.realmName = peopleNode
-
- if queryUserRecords:
- if self.peopleNode == userNode: # use sane directory and node if they are equal
- self.userNode = self.peopleNode
- self.userDirectory = self.peopleDirectory
- else:
- self.userNode = userNode
- try:
- self.userDirectory = self.odModule.odInit(userNode)
- except self.odModule.ODError, e:
- self.log_error("Open Directory (node=%s) Initialization error: %s" % (userNode, e))
- raise
- if self.realmName:
- self.realmName += "+" + userNode
- else:
- self.realmName = userNode
-
-
- self.maxDSQueryRecords = maxDSQueryRecords
-
- self.ignoreSystemRecords = ignoreSystemRecords
- self.queryDSLocal = queryDSLocal
- self.dsLocalCacheTimeout = dsLocalCacheTimeout
-
- self.fakeETag = fakeETag
-
- self.addDSAttrXProperties = addDSAttrXProperties
- self.generateSimpleUIDs = generateSimpleUIDs
- self.appleInternalServer = appleInternalServer
-
- self.additionalAttributes = additionalAttributes
- # filter allows attributes, but make sure there are a minimum of attributes for functionality
- if allowedAttributes:
- self.allowedDSQueryAttributes = sorted(list(set(
- [attr for attr in ABDirectoryQueryResult.allDSQueryAttributes
- if (isinstance(attr, str) and attr in allowedAttributes) or
- (isinstance(attr, tuple) and attr[0] in allowedAttributes)] +
- ABDirectoryQueryResult.dsqueryAttributesForProperty.get("X-INTERNAL-REQUIRED")
- )))
- if (self.allowedDSQueryAttributes != ABDirectoryQueryResult.allDSQueryAttributes):
- self.log_info("Allowed DS query attributes = %r" % (self.allowedDSQueryAttributes, ))
- else:
- self.allowedDSQueryAttributes = ABDirectoryQueryResult.allDSQueryAttributes
-
- #self.returnedAttributes = ABDirectoryQueryResult.allDSQueryAttributes
- self.returnedAttributes = self.allowedDSQueryAttributes
-
-
-
-
- self._dsLocalResults = {}
- self._nextDSLocalQueryTime = 0
-
- def __cmp__(self, other):
- if not isinstance(other, DirectoryRecord):
- return super(DirectoryRecord, self).__eq__(other)
-
- for attr in ("directory", "node"):
- diff = cmp(getattr(self, attr), getattr(other, attr))
- if diff != 0:
- return diff
- return 0
-
- def __hash__(self):
- h = hash(self.__class__.__name__)
- for attr in ("node",):
- h = (h + hash(getattr(self, attr))) & sys.maxint
- return h
-
- def createCache(self):
- succeed(None)
-
- def _getAllDSLocalResults(self):
-
- def generateDSLocalResults():
-
- resultsDictionary = {}
-
- recordTypes = [dsattributes.kDSStdRecordTypePeople, dsattributes.kDSStdRecordTypeUsers, ]
- try:
- localNodeDirectory = self.odModule.odInit("/Local/Default")
- self.log_debug("opendirectory.listAllRecordsWithAttributes_list(%r,%r,%r)" % (
- "/DSLocal",
- recordTypes,
- self.returnedAttributes,
- ))
- records = list(self.odModule.listAllRecordsWithAttributes_list(
- localNodeDirectory,
- recordTypes,
- self.returnedAttributes,
- ))
- except self.odModule.ODError, ex:
- self.log_error("Open Directory (node=%s) error: %s" % ("/Local/Default", str(ex)))
- raise
-
- for (recordShortName, value) in records: #@UnusedVariable
-
- try:
- result = ABDirectoryQueryResult(self.directoryBackedAddressBook, value)
- except:
- traceback.print_exc()
- self.log_info("Could not get vcard for record %s" % (recordShortName,))
-
- else:
- uid = result.vCard().propertyValue("UID")
-
- if self.ignoreSystemRecords:
- # remove system users and people
- if uid.startswith("FFFFEEEE-DDDD-CCCC-BBBB-AAAA"):
- self.log_info("Ignoring vcard for system record %s" % (recordShortName,))
- continue
-
- if uid in resultsDictionary:
- self.log_info("Record skipped due to duplicate UID: %s" % (recordShortName,))
- continue
-
- self.log_debug("VCard text =\n%s" % (result.vCardText(), ))
- resultsDictionary[uid] = result
-
-
- return resultsDictionary
-
-
- if not self.queryDSLocal:
- return {}
-
- if time.time() > self._nextDSLocalQueryTime:
- self._dsLocalResults = generateDSLocalResults()
- # Add jitter/fuzz factor
- self._nextDSLocalQueryTime = time.time() + self.dsLocalCacheTimeout * (random() + 0.5) * 60
-
- return self._dsLocalResults
-
-
- @inlineCallbacks
- def _getDirectoryQueryResults(self, query=None, attributes=None, maxRecords=0 ):
- """
- Get a list of filtered ABDirectoryQueryResult for the given query with the given attributes.
- query == None gets all records. attribute == None gets ABDirectoryQueryResult.allDSQueryAttributes
- """
- limited = False
- records = (yield self._queryDirectory(query, attributes, maxRecords ))
- if maxRecords and len(records) >= maxRecords:
- limited = True
- self.log_debug("Directory address book record limit (= %d) reached." % (maxRecords, ))
-
- self.log_debug("Query done. Inspecting %s records" % len(records))
-
- resultsDictionary = self._getAllDSLocalResults().copy()
- self.log_debug("Adding %s DSLocal results" % len(resultsDictionary.keys()))
-
- for (recordShortName, recordAttributes) in records: #@UnusedVariable
-
- try:
- # fix ds strangeness
- if recordAttributes.get(dsattributes.kDS1AttrLastName, "") == "99":
- del recordAttributes[dsattributes.kDS1AttrLastName]
-
- result = ABDirectoryQueryResult(self.directoryBackedAddressBook, recordAttributes,
- generateSimpleUIDs=self.generateSimpleUIDs,
- addDSAttrXProperties=self.addDSAttrXProperties,
- appleInternalServer=self.appleInternalServer,
- )
- except:
- traceback.print_exc()
- self.log_info("Could not get vcard for record %s" % (recordShortName,))
-
- else:
- uid = result.vCard().propertyValue("UID")
-
- if self.ignoreSystemRecords:
- # remove system users and people
- if uid.startswith("FFFFEEEE-DDDD-CCCC-BBBB-AAAA"):
- self.log_info("Ignoring vcard for system record %s" % (recordShortName,))
- continue
-
- if uid in resultsDictionary:
- self.log_info("Record skipped due to duplicate UID: %s" % (recordShortName,))
- continue
-
- self.log_debug("VCard text =\n%s" % (result.vCardText(), ))
- resultsDictionary[uid] = result
-
- self.log_debug("After filtering, %s results (limited=%s)." % (len(resultsDictionary), limited))
- returnValue((resultsDictionary.values(), limited, ))
-
-
- def _queryDirectory(self, query=None, attributes=None, maxRecords=0 ):
-
- startTime = time.time()
-
-
- if not attributes:
- attributes = self.returnedAttributes
-
- attributes = list(set(attributes + self.additionalAttributes)) # remove duplicates
-
- directoryAndRecordTypes = []
- if self.peopleDirectory == self.userDirectory:
- # use single ds query if possible for best performance
- directoryAndRecordTypes.append( (self.peopleDirectory, self.peopleNode, (dsattributes.kDSStdRecordTypePeople, dsattributes.kDSStdRecordTypeUsers) ) )
- else:
- if self.peopleDirectory:
- directoryAndRecordTypes.append( (self.peopleDirectory, self.peopleNode, dsattributes.kDSStdRecordTypePeople) )
- if self.userDirectory:
- directoryAndRecordTypes.append( (self.userDirectory, self.userNode, dsattributes.kDSStdRecordTypeUsers) )
-
- allResults = []
- for directory, node, recordType in directoryAndRecordTypes:
- try:
- if query:
- if isinstance(query, dsquery.match) and query.value is not "":
- self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r,%r)" % (
- node,
- query.attribute,
- query.value,
- query.matchType,
- False,
- recordType,
- attributes,
- maxRecords,
- ))
- results = list(
- self.odModule.queryRecordsWithAttribute_list(
- directory,
- query.attribute,
- query.value,
- query.matchType,
- False,
- recordType,
- attributes,
- maxRecords,
- ))
- else:
- self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r)" % (
- node,
- query.generate(),
- False,
- recordType,
- attributes,
- maxRecords,
- ))
- results = list(
- self.odModule.queryRecordsWithAttributes_list(
- directory,
- query.generate(),
- False,
- recordType,
- attributes,
- maxRecords,
- ))
- else:
- self.log_debug("opendirectory.listAllRecordsWithAttributes_list(%r,%r,%r,%r)" % (
- node,
- recordType,
- attributes,
- maxRecords,
- ))
- results = list(
- self.odModule.listAllRecordsWithAttributes_list(
- directory,
- recordType,
- attributes,
- maxRecords,
- ))
- except self.odModule.ODError, ex:
- self.log_error("Open Directory (node=%s) error: %s" % (self.realmName, str(ex)))
- raise
-
- allResults.extend(results)
-
- if maxRecords:
- maxRecords -= len(results)
- if maxRecords <= 0:
- break
-
-
- elaspedTime = time.time()-startTime
- self.log_info("Timing: Directory query: %.1f ms (%d records, %.2f records/sec)" % (elaspedTime*1000, len(allResults), len(allResults)/elaspedTime))
- return succeed(allResults)
-
- def _attributesForAddressBookQuery(self, addressBookQuery ):
-
- etagRequested, propertyNames = propertiesInAddressBookQuery( addressBookQuery )
-
- if etagRequested and not self.fakeETag:
- propertyNames = None
-
- if not propertyNames:
- #print("using all attributes")
- return self.returnedAttributes
-
- else:
- propertyNames.append("X-INTERNAL-MINIMUM-VCARD-PROPERTIES") # these properties are required to make a vCard
- queryAttributes = []
- for prop in propertyNames:
- if prop in ABDirectoryQueryResult.dsqueryAttributesForProperty:
- #print("adding attributes %r" % ABDirectoryQueryResult.dsqueryAttributesForProperty.get(prop))
- queryAttributes += ABDirectoryQueryResult.dsqueryAttributesForProperty.get(prop)
-
- return list(set(queryAttributes).intersection(set(self.returnedAttributes)))
-
-
-
- @inlineCallbacks
- def doAddressBookQuery(self, addressBookFilter, addressBookQuery, maxResults ):
- """
- Get vCards for a given addressBookFilter and addressBookQuery
- """
-
- allRecords, filterAttributes, dsFilter = dsFilterFromAddressBookFilter( addressBookFilter, vcardPropToLdapAttrMap=ABDirectoryQueryResult.dsqueryAttributesForProperty, allowedAttributes=self.allowedDSQueryAttributes );
- #print("allRecords = %s, query = %s" % (allRecords, "None" if dsFilter is None else dsFilter.generate(),))
-
- # testing:
- # allRecords = True
-
- if allRecords:
- dsFilter = None # None expression == all Records
- clear = not allRecords and not dsFilter
-
- results = []
- limited = False
-
- if not clear:
-
- # add filter to ignore system records rather than post filtering
- # but this appears to be broken in open directory
- '''
- if self.ignoreSystemRecords:
- ignoreExpression = dsquery.expression( dsquery.expression.NOT,
- dsquery.match(dsattributes.kDS1AttrGeneratedUID, "FFFFEEEE-DDDD-CCCC-BBBB-AAAA", dsattributes.eDSStartsWith)
- )
- filterAttributes = list(set(filterAttributes).union(dsattributes.kDS1AttrGeneratedUID))
-
- dsFilter = dsquery.expression( dsquery.expression.AND, (dsFilter, ignoreExpression,) ) if dsFilter else ignoreExpression
- #dsFilter = ignoreExpression
- '''
-
- queryAttributes = self._attributesForAddressBookQuery( addressBookQuery )
- attributes = filterAttributes + queryAttributes
-
- #calc maxRecords from passed in maxResults allowing extra for second stage filtering in caller
- maxRecords = int(maxResults * 1.2)
- if self.maxDSQueryRecords and maxRecords > self.maxDSQueryRecords:
- maxRecords = self.maxDSQueryRecords
-
- results, limited = (yield self._getDirectoryQueryResults(dsFilter, attributes, maxRecords))
-
- returnValue((results, limited,))
-
-
-#utility
-def propertiesInAddressBookQuery( addressBookQuery ):
-
- etagRequested = False
- propertyNames = []
- #print( "addressBookQuery.qname=%r" % addressBookQuery.qname)
- if addressBookQuery.qname() == ("DAV:", "prop"):
-
- for property in addressBookQuery.children:
- #print("property = %r" % property )
- if isinstance(property, carddavxml.AddressData):
- for addressProperty in property.children:
- #print("addressProperty = %r" % addressProperty )
- if isinstance(addressProperty, carddavxml.Property):
- #print("Adding property %r", addressProperty.attributes["name"])
- propertyNames.append(addressProperty.attributes["name"])
-
- elif property.qname() == ("DAV:", "getetag"):
- # for a real etag == md5(vCard), we need all attributes
- etagRequested = True
-
- return (etagRequested, propertyNames if len(propertyNames) else None)
-
-
-def dsFilterFromAddressBookFilter(addressBookFilter, vcardPropToLdapAttrMap, allowedAttributes=None):
- """
- Convert the supplied addressbook-query into a ds expression tree.
-
- @param filter: the L{Filter} for the addressbook-query to convert.
- @return: (needsAllRecords, espressionAttributes, expression) tuple
- """
- def propFilterListQuery(filterAllOf, propFilters):
-
- def propFilterExpression(filterAllOf, propFilter):
- #print("propFilterExpression")
- """
- Create an expression for a single prop-filter element.
-
- @param propFilter: the L{PropertyFilter} element.
- @return: (needsAllRecords, espressionAttributes, expressions) tuple
- """
-
- def definedExpression( defined, allOf, filterName, constant, queryAttributes, allAttrStrings):
- if constant or filterName in ("N" , "FN", "UID", ):
- return (defined, [], []) # all records have this property so no records do not have it
- else:
- matchList = list(set([dsquery.match(attrName, "", dsattributes.eDSStartsWith) for attrName in allAttrStrings]))
- if defined:
- # TODO: Investigate what happens when andOrExpresion() does not return an expression
- return andOrExpression(allOf, queryAttributes, matchList)
- else:
- if len(matchList) > 1:
- expr = dsquery.expression( dsquery.expression.OR, matchList )
- else:
- expr = matchList[0]
- return (False, queryAttributes, [dsquery.expression( dsquery.expression.NOT, expr),])
- #end isNotDefinedExpression()
-
-
- def andOrExpression(propFilterAllOf, queryAttributes, matchList):
- #print("andOrExpression(propFilterAllOf=%r, queryAttributes%r, matchList%r)" % (propFilterAllOf, queryAttributes, matchList))
- if propFilterAllOf and len(matchList) > 1:
- # add OR expression because parent will AND
- return (False, queryAttributes, [dsquery.expression( dsquery.expression.OR, matchList),])
- else:
- return (False, queryAttributes, matchList)
- #end andOrExpression()
-
-
- # short circuit parameter filters
- def supportedParamter( filterName, paramFilters, propFilterAllOf ):
-
- def supported( paramFilterName, paramFilterDefined, params ):
- paramFilterName = paramFilterName.upper()
- if len(params.keys()) and ((paramFilterName in params.keys()) != paramFilterDefined):
- return False
- if len(params[paramFilterName]) and str(paramFilter.qualifier).upper() not in params[paramFilterName]:
- return False
- return True
- #end supported()
-
-
- oneSupported = False
- for paramFilter in paramFilters:
- if filterName == "PHOTO":
- if propFilterAllOf != supported( paramFilter.filter_name, paramFilter.defined, { "ENCODING": ["B",], "TYPE": ["JPEG",], }):
- return not propFilterAllOf
- oneSupported |= propFilterAllOf
- elif filterName == "ADR":
- if propFilterAllOf != supported( paramFilter.filter_name, paramFilter.defined, { "TYPE": ["WORK", "PREF", "POSTAL", "PARCEL",], }):
- return not propFilterAllOf
- oneSupported |= propFilterAllOf
- elif filterName == "LABEL":
- if propFilterAllOf != supported( paramFilter.filter_name, paramFilter.defined, { "TYPE": ["POSTAL", "PARCEL",]}):
- return not propFilterAllOf
- oneSupported |= propFilterAllOf
- elif filterName == "TEL":
- if propFilterAllOf != supported( paramFilter.filter_name, paramFilter.defined, { "TYPE": [], }): # has params derived from ds attributes
- return not propFilterAllOf
- oneSupported |= propFilterAllOf
- elif filterName == "EMAIL":
- if propFilterAllOf != supported( paramFilter.filter_name, paramFilter.defined, { "TYPE": [], }): # has params derived from ds attributes
- return not propFilterAllOf
- oneSupported |= propFilterAllOf
- elif filterName == "URL":
- if propFilterAllOf != supported( paramFilter.filter_name, paramFilter.defined, {}):
- return not propFilterAllOf
- oneSupported |= propFilterAllOf
- elif filterName == "KEY":
- if propFilterAllOf != supported( paramFilter.filter_name, paramFilter.defined, { "ENCODING": ["B",], "TYPE": ["PGPPUBILICKEY", "USERCERTIFICATE", "USERPKCS12DATA", "USERSMIMECERTIFICATE",] }):
- return not propFilterAllOf
- oneSupported |= propFilterAllOf
- elif not filterName.startswith("X-"): #X- IMHandles X-ABRELATEDNAMES excepted, no other params are used
- if propFilterAllOf == paramFilter.defined:
- return not propFilterAllOf
- oneSupported |= propFilterAllOf
-
- if propFilterAllOf:
- return True
- else:
- return oneSupported
- #end supportedParamter()
-
-
- def textMatchElementExpression( propFilterAllOf, textMatchElement ):
-
- # pre process text match strings for ds query
- def getMatchStrings( propFilter, matchString ):
-
- if propFilter.filter_name in ("REV" , "BDAY", ):
- rawString = matchString
- matchString = ""
- for c in rawString:
- if not c in "TZ-:":
- matchString += c
- elif propFilter.filter_name == "GEO":
- matchString = ",".join(matchString.split(";"))
-
- if propFilter.filter_name in ("N" , "ADR", "ORG", ):
- # for structured properties, change into multiple strings for ds query
- if propFilter.filter_name == "ADR":
- #split by newline and comma
- rawStrings = ",".join( matchString.split("\n") ).split(",")
- else:
- #split by space
- rawStrings = matchString.split(" ")
-
- # remove empty strings
- matchStrings = []
- for oneString in rawStrings:
- if len(oneString):
- matchStrings += [oneString,]
- return matchStrings
-
- elif len(matchString):
- return [matchString,]
- else:
- return []
- # end getMatchStrings
-
- if constant:
- # do the match right now! Return either all or none.
- #FIXME: match is not implemented in twisteddaldav.query.addressbookqueryfilter.TextMatch so use _match for now
- return( textMatchElement._match([constant,]), [], [] )
- else:
-
- matchStrings = getMatchStrings(propFilter, textMatchElement.text)
-
- if not len(matchStrings) or binaryAttrStrs:
- # no searching text in binary ds attributes, so change to defined/not defined case
- if textMatchElement.negate:
- return definedExpression(False, propFilterAllOf, propFilter.filter_name, constant, queryAttributes, allAttrStrings)
- # else fall through to attribute exists case below
- else:
-
- # special case UID's formed from node and record name
- if propFilter.filter_name == "UID":
- matchString = matchStrings[0]
- seperatorIndex = matchString.find(ABDirectoryQueryResult.peopleUIDSeparator)
- if seperatorIndex > 1:
- recordNameStart = seperatorIndex + len(ABDirectoryQueryResult.peopleUIDSeparator)
- else:
- seperatorIndex = matchString.find(ABDirectoryQueryResult.userUIDSeparator)
- if seperatorIndex > 1:
- recordNameStart = seperatorIndex + len(ABDirectoryQueryResult.userUIDSeparator)
- else:
- recordNameStart = sys.maxint
-
- if recordNameStart < len(matchString)-1:
- try:
- recordNameQualifier = matchString[recordNameStart:].decode("base64").decode("utf8")
- except Exception, e:
- self.log_debug("Could not decode UID string %r in %r: %r" % (matchString[recordNameStart:], matchString, e,))
- else:
- if textMatchElement.negate:
- return (False, queryAttributes,
- [dsquery.expression(dsquery.expression.NOT, dsquery.match(dsattributes.kDSNAttrRecordName, recordNameQualifier, dsattributes.eDSExact)),]
- )
- else:
- return (False, queryAttributes,
- [dsquery.match(dsattributes.kDSNAttrRecordName, recordNameQualifier, dsattributes.eDSExact),]
- )
-
- # use match_type where possible depending on property/attribute mapping
- # Note that case sensitive negate will not work
- # Should return all records in that case
- matchType = dsattributes.eDSContains
- if propFilter.filter_name in ("NICKNAME" , "TITLE" , "NOTE" , "UID", "URL", "N", "ADR", "ORG", "REV", "LABEL", ):
- if textMatchElement.match_type == "equals":
- matchType = dsattributes.eDSExact
- elif textMatchElement.match_type == "starts-with":
- matchType = dsattributes.eDSStartsWith
- elif textMatchElement.match_type == "ends-with":
- matchType = dsattributes.eDSEndsWith
-
- matchList = []
- for matchString in matchStrings:
- matchList += [dsquery.match(attrName, matchString, matchType) for attrName in stringAttrStrs]
-
- matchList = list(set(matchList))
-
- if textMatchElement.negate:
- if len(matchList) > 1:
- expr = dsquery.expression( dsquery.expression.OR, matchList )
- else:
- expr = matchList[0]
- return (False, queryAttributes, [dsquery.expression( dsquery.expression.NOT, expr),])
- else:
- return andOrExpression(propFilterAllOf, queryAttributes, matchList)
-
- # attribute exists search
- return definedExpression(True, propFilterAllOf, propFilter.filter_name, constant, queryAttributes, allAttrStrings)
- #end textMatchElementExpression()
-
-
- # get attribute strings from dsqueryAttributesForProperty list
- #queryAttributes = list(set(ABDirectoryQueryResult.dsqueryAttributesForProperty.get(propFilter.filter_name, [])).intersection(set(self.allowedDSQueryAttributes)))
- queryAttributes = vcardPropToLdapAttrMap.get(propFilter.filter_name, [])
- if isinstance(queryAttributes, str):
- queryAttributes = [queryAttributes,]
- if allowedAttributes:
- queryAttributes = list(set(queryAttributes).intersection(set(allowedAttributes)))
-
- binaryAttrStrs = []
- stringAttrStrs = []
- for attr in queryAttributes:
- if isinstance(attr, tuple):
- binaryAttrStrs.append(attr[0])
- else:
- stringAttrStrs.append(attr)
- allAttrStrings = stringAttrStrs + binaryAttrStrs
- if not allAttrStrings:
- # not AllAttrStrings means propFilter.filter_name is not mapped
- # return None to try to match all items if this is the only property filter
- return (None, [], [])
-
- constant = ABDirectoryQueryResult.constantProperties.get(propFilter.filter_name)
- if propFilter.qualifier and isinstance(propFilter.qualifier, addressbookqueryfilter.IsNotDefined):
- return definedExpression(False, filterAllOf, propFilter.filter_name, constant, queryAttributes, allAttrStrings)
-
- paramFilterElements = [paramFilterElement for paramFilterElement in propFilter.filters if isinstance(paramFilterElement, addressbookqueryfilter.ParameterFilter)]
- textMatchElements = [textMatchElement for textMatchElement in propFilter.filters if isinstance(textMatchElement, addressbookqueryfilter.TextMatch)]
-
- # if only one propFilter, then use filterAllOf as propFilterAllOf to reduce subexpressions and simplify generated query string
- if len(propFilter.filters) == 1:
- propFilterAllOf = filterAllOf
- else:
- propFilterAllOf = propFilter.propfilter_test == "allof"
-
- # handle parameter filter elements
- if len(paramFilterElements) > 0:
- if supportedParamter(propFilter.filter_name, paramFilterElements, propFilterAllOf ):
- if len(textMatchElements) == 0:
- return definedExpression(True, filterAllOf, propFilter.filter_name, constant, queryAttributes, allAttrStrings)
- else:
- if propFilterAllOf:
- return (False, [], [])
-
- # handle text match elements
- propFilterNeedsAllRecords = propFilterAllOf
- propFilterAttributes = []
- propFilterExpressionList = []
- for textMatchElement in textMatchElements:
-
- textMatchNeedsAllRecords, textMatchExpressionAttributes, textMatchExpression = textMatchElementExpression(propFilterAllOf, textMatchElement)
- if propFilterAllOf:
- propFilterNeedsAllRecords &= textMatchNeedsAllRecords
- else:
- propFilterNeedsAllRecords |= textMatchNeedsAllRecords
- propFilterAttributes += textMatchExpressionAttributes
- propFilterExpressionList += textMatchExpression
-
-
- if (len(propFilterExpressionList) > 1) and (filterAllOf != propFilterAllOf):
- propFilterExpressions = [dsquery.expression(dsquery.expression.AND if propFilterAllOf else dsquery.expression.OR , list(set(propFilterExpressionList)))] # remove duplicates
- else:
- propFilterExpressions = list(set(propFilterExpressionList))
-
- return (propFilterNeedsAllRecords, propFilterAttributes, propFilterExpressions)
- #end propFilterExpression
-
- #print("propFilterListQuery: filterAllOf=%r, propFilters=%r" % (filterAllOf, propFilters,))
- """
- Create an expression for a list of prop-filter elements.
-
- @param filterAllOf: the C{True} if parent filter test is "allof"
- @param propFilters: the C{list} of L{ComponentFilter} elements.
- @return: (needsAllRecords, espressionAttributes, expression) tuple
- """
- needsAllRecords = None
- attributes = []
- expressions = []
- for propFilter in propFilters:
-
- propNeedsAllRecords, propExpressionAttributes, propExpression = propFilterExpression(filterAllOf, propFilter)
- if needsAllRecords is None:
- needsAllRecords = propNeedsAllRecords
- elif propNeedsAllRecords is not None:
- if filterAllOf:
- needsAllRecords &= propNeedsAllRecords
- else:
- needsAllRecords |= propNeedsAllRecords
- attributes += propExpressionAttributes
- expressions += propExpression
- # propFilterExpression()'s returned propNeedsAllRecords is only None if a propFilter.filter_name is not not mapped
- # needsAllRecords is None if there was only one propFilter that returned None
- # set needsAllRecords True in the case
- if needsAllRecords is None:
- needsAllRecords = not filterAllOf
-
- if len(expressions) > 1:
- expr = dsquery.expression(dsquery.expression.AND if filterAllOf else dsquery.expression.OR , list(set(expressions))) # remove duplicates
- elif len(expressions):
- expr = expressions[0]
- else:
- expr = None
-
- return (needsAllRecords, attributes, expr)
-
-
- #print("_dsFilterFromAddressBookFilter")
- # Lets assume we have a valid filter from the outset
-
- # Top-level filter contains zero or more prop-filters
- if addressBookFilter:
- filterAllOf = addressBookFilter.filter_test == "allof"
- if len(addressBookFilter.children) > 0:
- return propFilterListQuery(filterAllOf, addressBookFilter.children)
- else:
- return (filterAllOf, [], [])
- else:
- return (False, [], [])
-
-
-
-class ABDirectoryQueryResult(DAVPropertyMixIn, LoggingMixIn):
- """
- Result from ab query report or multiget on directory
- """
-
- # od attributes that may contribute to vcard properties
- # will be used to translate vCard queries to od queries
-
- dsqueryAttributesForProperty = {
-
- "FN" : [
- dsattributes.kDS1AttrFirstName,
- dsattributes.kDS1AttrLastName,
- dsattributes.kDS1AttrMiddleName,
- dsattributes.kDSNAttrNamePrefix,
- dsattributes.kDSNAttrNameSuffix,
- dsattributes.kDS1AttrDistinguishedName,
- dsattributes.kDSNAttrRecordName,
- ],
- "N" : [
- dsattributes.kDS1AttrFirstName,
- dsattributes.kDS1AttrLastName,
- dsattributes.kDS1AttrMiddleName,
- dsattributes.kDSNAttrNamePrefix,
- dsattributes.kDSNAttrNameSuffix,
- dsattributes.kDS1AttrDistinguishedName,
- dsattributes.kDSNAttrRecordName,
- ],
- "NICKNAME" : [
- dsattributes.kDSNAttrNickName,
- ],
- # no binary searching
- "PHOTO" : [
- (dsattributes.kDSNAttrJPEGPhoto, "base64"),
- ],
- "BDAY" : [
- dsattributes.kDS1AttrBirthday,
- ],
- "ADR" : [
- dsattributes.kDSNAttrBuilding,
- dsattributes.kDSNAttrStreet,
- dsattributes.kDSNAttrCity,
- dsattributes.kDSNAttrState,
- dsattributes.kDSNAttrPostalCode,
- dsattributes.kDSNAttrCountry,
- ],
- "LABEL" : [
- dsattributes.kDSNAttrPostalAddress,
- dsattributes.kDSNAttrPostalAddressContacts,
- dsattributes.kDSNAttrAddressLine1,
- dsattributes.kDSNAttrAddressLine2,
- dsattributes.kDSNAttrAddressLine3,
- ],
- "TEL" : [
- dsattributes.kDSNAttrPhoneNumber,
- dsattributes.kDSNAttrMobileNumber,
- dsattributes.kDSNAttrPagerNumber,
- dsattributes.kDSNAttrHomePhoneNumber,
- dsattributes.kDSNAttrPhoneContacts,
- dsattributes.kDSNAttrFaxNumber,
- #dsattributes.kDSNAttrAreaCode,
- ],
- "EMAIL" : [
- dsattributes.kDSNAttrEMailAddress,
- dsattributes.kDSNAttrEMailContacts,
- ],
- "GEO" : [
- dsattributes.kDSNAttrMapCoordinates,
- ],
- "TITLE" : [
- dsattributes.kDSNAttrJobTitle,
- ],
- "ORG" : [
- dsattributes.kDSNAttrCompany,
- dsattributes.kDSNAttrOrganizationName,
- dsattributes.kDSNAttrDepartment,
- ],
- "NOTE" : [
- dsattributes.kDS1AttrComment,
- dsattributes.kDS1AttrNote,
- ],
- "REV" : [
- dsattributes.kDS1AttrModificationTimestamp,
- ],
- "UID" : [
- dsattributes.kDS1AttrGeneratedUID,
- # special cased
- #dsattributes.kDSNAttrMetaNodeLocation,
- #dsattributes.kDSNAttrRecordName,
- #dsattributes.kDS1AttrDistinguishedName,
- ],
- "URL" : [
- dsattributes.kDS1AttrWeblogURI,
- dsattributes.kDSNAttrURL,
- ],
- "KEY" : [
- # check on format, are these all binary?
- (dsattributes.kDSNAttrPGPPublicKey, "base64"),
- (dsattributes.kDS1AttrUserCertificate, "base64"),
- (dsattributes.kDS1AttrUserPKCS12Data, "base64"),
- (dsattributes.kDS1AttrUserSMIMECertificate, "base64"),
- ],
- "IMPP" : [
- dsattributes.kDSNAttrIMHandle,
- ],
- "X-AIM" : [
- dsattributes.kDSNAttrIMHandle,
- ],
- "X-JABBER" : [
- dsattributes.kDSNAttrIMHandle,
- ],
- "X-MSN" : [
- dsattributes.kDSNAttrIMHandle,
- ],
- "X-YAHOO" : [
- dsattributes.kDSNAttrIMHandle,
- ],
- "X-ICQ" : [
- dsattributes.kDSNAttrIMHandle,
- ],
- "X-ABRELATEDNAMES" : [
- dsattributes.kDSNAttrRelationships,
- ],
- "X-INTERNAL-MINIMUM-VCARD-PROPERTIES" : [
- dsattributes.kDS1AttrGeneratedUID,
- dsattributes.kDSNAttrMetaNodeLocation,
- dsattributes.kDS1AttrFirstName,
- dsattributes.kDS1AttrLastName,
- dsattributes.kDS1AttrMiddleName,
- dsattributes.kDSNAttrNamePrefix,
- dsattributes.kDSNAttrNameSuffix,
- dsattributes.kDS1AttrDistinguishedName,
- dsattributes.kDSNAttrRecordName,
- dsattributes.kDSNAttrRecordType,
- dsattributes.kDS1AttrModificationTimestamp,
- dsattributes.kDS1AttrCreationTimestamp,
- ],
- "X-INTERNAL-REQUIRED" : [
- dsattributes.kDS1AttrGeneratedUID,
- dsattributes.kDSNAttrMetaNodeLocation,
- dsattributes.kDS1AttrDistinguishedName,
- dsattributes.kDSNAttrRecordName,
- dsattributes.kDS1AttrFirstName,
- dsattributes.kDS1AttrLastName,
- dsattributes.kDSNAttrRecordType,
- ],
-
- }
-
-
- allDSQueryAttributes = sorted(list(set([attr for lookupAttributes in dsqueryAttributesForProperty.values()
- for attr in lookupAttributes])))
-
- binaryDSAttributeStrs = [attr[0] for attr in allDSQueryAttributes
- if isinstance(attr, tuple) ]
-
- stringDSAttributeStrs = [attr for attr in allDSQueryAttributes
- if isinstance(attr, str) ]
-
- allDSAttributeStrs = stringDSAttributeStrs + binaryDSAttributeStrs
-
- #peopleUIDSeparator = "-" + OpenDirectoryBackingService.baseGUID + "-"
- userUIDSeparator = "-bf07a1a2-"
- peopleUIDSeparator = "-cf07a1a2-"
-
-
- constantProperties = {
- # 3.6.3 PRODID Type Definition
- "PRODID": vCardProductID,
- # 3.6.9 VERSION Type Definition
- "VERSION": "3.0",
- }
-
-
- def __init__(self, directoryBackedAddressBook, recordAttributes, generateSimpleUIDs=False, addDSAttrXProperties=False, appleInternalServer=False, ):
-
-
- self.log_debug("directoryBackedAddressBook=%s, attributes=%s" % (directoryBackedAddressBook, recordAttributes))
-
- #save off for debugging
- self.addDSAttrXProperties = addDSAttrXProperties;
- if addDSAttrXProperties:
- self.originalAttributes = recordAttributes.copy()
- self.appleInternalServer = appleInternalServer
-
- self._directoryBackedAddressBook = directoryBackedAddressBook
- self._vCard = None
-
- #clean attributes
- self.attributes = {}
- for key, values in recordAttributes.items():
- if key in ABDirectoryQueryResult.stringDSAttributeStrs:
- if isinstance(values, list):
- self.attributes[key] = [removeControlChars(val).decode("utf8") for val in values]
- else:
- self.attributes[key] = removeControlChars(values).decode("utf8")
- else:
- self.attributes[key] = values
-
- # find a GUID
- guid = self.firstValueForAttribute(dsattributes.kDS1AttrGeneratedUID)
- if not guid:
- if generateSimpleUIDs:
- nodeUUIDStr = "00000000"
- else:
- nodeUUIDStr = "%x" % abs(hash(node))
- nameUUIDStr = "".join(self.firstValueForAttribute(dsattributes.kDSNAttrRecordName).encode("base64").split("\n"))
- if self.firstValueForAttribute(dsattributes.kDSNAttrRecordType) != dsattributes.kDSStdRecordTypePeople:
- guid = ABDirectoryQueryResult.userUIDSeparator.join([nodeUUIDStr, nameUUIDStr,])
- else:
- guid = ABDirectoryQueryResult.peopleUIDSeparator.join([nodeUUIDStr, nameUUIDStr,])
-
- self.attributes[dsattributes.kDS1AttrGeneratedUID] = guid
-
- #generate a vCard here. May throw an exception
- self.vCard()
-
-
-
- def __repr__(self):
- return "<%s[%s(%s)]>" % (
- self.__class__.__name__,
- self.vCard().propertyValue("FN"),
- self.vCard().propertyValue("UID")
- )
-
- def __hash__(self):
- s = "".join([
- "%s:%s" % (attribute, self.valuesForAttribute(attribute),)
- for attribute in self.attributes
- ])
- return hash(s)
-
-
- def hasAttribute(self, attributeName ):
- return self.valuesForAttribute(attributeName, None) is not None
-
-
- def valuesForAttribute(self, attributeName, default_values=[] ):
- values = self.attributes.get(attributeName)
- if (values is None):
- return default_values
- elif not isinstance(values, list):
- values = [values, ]
-
- # ds templates often return empty attribute values
- # get rid of them here
- nonEmptyValues = [(value.encode("utf-8") if isinstance(value, unicode) else value) for value in values if len(value) > 0 ]
-
- if len(nonEmptyValues) > 0:
- return nonEmptyValues
- else:
- return default_values
-
-
- def firstValueForAttribute(self, attributeName, default_value="" ):
- values = self.attributes.get(attributeName)
- if values is None:
- return default_value
- elif isinstance(values, list):
- return values[0].encode("utf_8") if isinstance(values[0], unicode) else values[0]
- else:
- return values.encode("utf_8") if isinstance(values, unicode) else values
-
- def joinedValuesForAttribute(self, attributeName, separator=",", default_string="" ):
- values = self.valuesForAttribute(attributeName, None)
- if not values:
- return default_string
- else:
- return separator.join(values)
-
-
- def isoDateStringForDateAttribute(self, attributeName, default_string="" ):
- modDate = self.firstValueForAttribute(attributeName, default_string)
- revDate = None
- if modDate:
- if len(modDate) >= len("YYYYMMDD") and modDate[:8].isdigit():
- revDate = "%s-%s-%s" % (modDate[:4],modDate[4:6],modDate[6:8], )
- if len(modDate) >= len("YYYYMMDDHHMMSS") and modDate[8:14].isdigit():
- revDate += "T%s:%s:%sZ" % (modDate[8:10],modDate[10:12],modDate[12:14], )
- return revDate
-
-
-
- def vCard(self):
-
-
- def generateVCard():
-
- def isUniqueProperty(vcard, newProperty, ignoreParams = None):
- existingProperties = vcard.properties(newProperty.name())
- for existingProperty in existingProperties:
- if ignoreParams:
- existingProperty = existingProperty.duplicate()
- for paramname, paramvalue in ignoreParams:
- existingProperty.removeParameterValue(paramname, paramvalue)
- if existingProperty == newProperty:
- return False
- return True
-
- def addUniqueProperty(vcard, newProperty, ignoreParams = None, attrType = None, attrValue = None):
- if isUniqueProperty(vcard, newProperty, ignoreParams):
- vcard.addProperty(newProperty)
- else:
- if attrType and attrValue:
- self.log_info("Ignoring attribute %r with value %r in creating property %r. A duplicate property already exists." % (attrType, attrValue, newProperty, ))
-
- def addPropertyAndLabel(groupCount, label, propertyName, propertyValue, parameters = None ):
- groupCount[0] += 1
- groupPrefix = "item%d" % groupCount[0]
- vcard.addProperty(Property(propertyName, propertyValue, params=parameters, group=groupPrefix))
- vcard.addProperty(Property("X-ABLabel", label, group=groupPrefix))
-
- # for attributes of the form param:value
- def addPropertiesAndLabelsForPrefixedAttribute(groupCount, propertyPrefix, propertyName, attrType, defaultLabel, nolabelParamTypes=(), labelMap={}, specialParamType=None):
- preferred = True
- for attrValue in self.valuesForAttribute(attrType):
- try:
- # special case for Apple
- if self.appleInternalServer and attrType == dsattributes.kDSNAttrIMHandle:
- splitValue = attrValue.split("|")
- if len (splitValue) > 1:
- attrValue = splitValue[0]
- if splitValue[1].upper() in nolabelParamTypes:
- defaultLabel = splitValue[1]
-
- colonIndex = attrValue.find(":")
- if (colonIndex > len(attrValue)-2):
- raise ValueError("Nothing after colon.")
-
- propertyValue = attrValue[colonIndex+1:]
- labelString = attrValue[:colonIndex] if colonIndex > 0 else defaultLabel
- paramTypeString = labelString.upper()
-
- if specialParamType:
- parameters = { specialParamType: (paramTypeString,) }
- if preferred:
- parameters["TYPE"] = ("PREF",)
- else:
- # add PREF to first prop's parameters
- paramTypeStrings = [paramTypeString,]
- if preferred and "PREF" != paramTypeString:
- paramTypeStrings += ["PREF",]
- parameters = { "TYPE": paramTypeStrings, }
-
- #special case for IMHandles which the param is the last part of the property like X-AIM or X-JABBER
- if propertyPrefix:
- propertyName = propertyPrefix + paramTypeString
-
- # only add label prop if needed
- if paramTypeString in nolabelParamTypes:
- addUniqueProperty(vcard, Property(propertyName, attrValue[colonIndex+1:], params=parameters), None, attrValue, attrType)
- else:
- # use special localizable addressbook labels where possible
- localizedABLabelString = labelMap.get(labelString, labelString)
- addPropertyAndLabel(groupCount, localizedABLabelString, propertyName, propertyValue, parameters)
- preferred = False
-
- except Exception, e:
- traceback.print_exc()
- self.log_debug("addPropertiesAndLabelsForPrefixedAttribute(): groupCount=%r, propertyPrefix=%r, propertyName=%r, nolabelParamTypes=%r, labelMap=%r, attrType=%r" % (groupCount[0], propertyPrefix, propertyName, nolabelParamTypes, labelMap, attrType,))
- self.log_error("addPropertiesAndLabelsForPrefixedAttribute(): Trouble parsing attribute %s, with value \"%s\". Error = %s" % (attrType, attrValue, e,))
-
-
- #print("ABDirectoryQueryResult.vCard")
- # create vCard
- vcard = Component("VCARD")
- groupCount = [0]
-
- # add constant properties - properties that are the same regardless of the record attributes
- for key, value in ABDirectoryQueryResult.constantProperties.items():
- vcard.addProperty(Property(key, value))
-
- # 3.1 IDENTIFICATION TYPES http://tools.ietf.org/html/rfc2426#section-3.1
- # 3.1.1 FN Type Definition
- # dsattributes.kDS1AttrDistinguishedName, # Users distinguished or real name
- #
- # full name is required but this is set in OpenDiretoryBackingRecord.__init__
- #vcard.addProperty(Property("FN", self.firstValueForAttribute(dsattributes.kDS1AttrDistinguishedName)))
-
- # 3.1.2 N Type Definition
- # dsattributes.kDS1AttrFirstName, # Used for first name of user or person record.
- # dsattributes.kDS1AttrLastName, # Used for the last name of user or person record.
- # dsattributes.kDS1AttrMiddleName, # Used for the middle name of user or person record.
- # dsattributes.kDSNAttrNameSuffix, # Represents the name suffix of a user or person.
- # ie. Jr., Sr., etc.
- # Usually found in user or people records (kDSStdRecordTypeUsers or
- # dsattributes.kDSStdRecordTypePeople).
- # dsattributes.kDSNAttrNamePrefix, # Represents the title prefix of a user or person.
- # ie. Mr., Ms., Mrs., Dr., etc.
- # Usually found in user or people records (kDSStdRecordTypeUsers or
- # dsattributes.kDSStdRecordTypePeople).
-
- # name is required, so make sure we have one
- # vcard says: Each name attribute can be a string or a list of strings.
- if not self.hasAttribute(dsattributes.kDS1AttrFirstName) and not self.hasAttribute(dsattributes.kDS1AttrLastName):
- familyName = self.firstValueForAttribute(dsattributes.kDS1AttrDistinguishedName)
- else:
- familyName = self.valuesForAttribute(dsattributes.kDS1AttrLastName, "")
-
- nameObject = N(
- first = self.valuesForAttribute(dsattributes.kDS1AttrFirstName, ""),
- last = familyName,
- middle = self.valuesForAttribute(dsattributes.kDS1AttrMiddleName, ""),
- prefix = self.valuesForAttribute(dsattributes.kDSNAttrNamePrefix, ""),
- suffix = self.valuesForAttribute(dsattributes.kDSNAttrNameSuffix, ""),
- )
- vcard.addProperty(Property("N", nameObject))
-
- # set full name to Name with contiguous spaces stripped
- # it turns out that Address Book.app ignores FN and creates it fresh from N in ABRecord
- # so no reason to have FN distinct from N
- vcard.addProperty(Property("FN", nameObject.getFullName() ))
-
- # 3.1.3 NICKNAME Type Definition
- # dsattributes.kDSNAttrNickName, # Represents the nickname of a user or person.
- # Usually found in user or people records (kDSStdRecordTypeUsers or
- # dsattributes.kDSStdRecordTypePeople).
- for nickname in self.valuesForAttribute(dsattributes.kDSNAttrNickName):
- addUniqueProperty(vcard, Property("NICKNAME", nickname), None, dsattributes.kDSNAttrNickName, nickname)
-
- # 3.1.4 PHOTO Type Definition
- # dsattributes.kDSNAttrJPEGPhoto, # Used to store binary picture data in JPEG format.
- # Usually found in user, people or group records (kDSStdRecordTypeUsers,
- # dsattributes.kDSStdRecordTypePeople,dsattributes.kDSStdRecordTypeGroups).
- # pyOpenDirectory always returns binary-encoded string
-
- for photo in self.valuesForAttribute(dsattributes.kDSNAttrJPEGPhoto):
- photo = "".join("".join(photo.split("\r")).split("\n")) #get rid of line folding: for PHOTO
- addUniqueProperty(vcard, Property("PHOTO", photo, params={"ENCODING": ["b",], "TYPE": ["JPEG",],}), None, dsattributes.kDSNAttrJPEGPhoto, photo)
-
-
- # 3.1.5 BDAY Type Definition
- # dsattributes.kDS1AttrBirthday, # Single-valued attribute that defines the user's birthday.
- # Format is x.208 standard YYYYMMDDHHMMSSZ which we will require as GMT time.
- # 012345678901234
-
- birthdate = self.isoDateStringForDateAttribute(dsattributes.kDS1AttrBirthday)
- if birthdate:
- vcard.addProperty(Property("BDAY", PyCalendarDateTime.parseText(birthdate, fullISO=True)))
-
-
- # 3.2 Delivery Addressing Types http://tools.ietf.org/html/rfc2426#section-3.2
- #
- # 3.2.1 ADR Type Definition
-
- #address
- # vcard says: Each address attribute can be a string or a list of strings.
- extended = self.valuesForAttribute(dsattributes.kDSNAttrBuilding, "")
- street = self.valuesForAttribute(dsattributes.kDSNAttrStreet, "")
- city = self.valuesForAttribute(dsattributes.kDSNAttrCity, "")
- region = self.valuesForAttribute(dsattributes.kDSNAttrState, "")
- code = self.valuesForAttribute(dsattributes.kDSNAttrPostalCode, "")
- country = self.valuesForAttribute(dsattributes.kDSNAttrCountry, "")
-
- if len(extended) > 0 or len(street) > 0 or len(city) > 0 or len(region) > 0 or len(code) > 0 or len(country) > 0:
- vcard.addProperty(Property("ADR",
- Adr(
- #pobox = box,
- extended = extended,
- street = street,
- locality = city,
- region = region,
- postalcode = code,
- country = country,
- ),
- params = {"TYPE": ["WORK", "PREF", "POSTAL", "PARCEL",],}
- ))
-
-
- # 3.2.2 LABEL Type Definition
-
- # dsattributes.kDSNAttrPostalAddress, # The postal address usually excluding postal code.
- # dsattributes.kDSNAttrPostalAddressContacts, # multi-valued attribute that defines a record's alternate postal addresses .
- # found in user records (kDSStdRecordTypeUsers) and resource records (kDSStdRecordTypeResources).
- # dsattributes.kDSNAttrAddressLine1, # Line one of multiple lines of address data for a user.
- # dsattributes.kDSNAttrAddressLine2, # Line two of multiple lines of address data for a user.
- # dsattributes.kDSNAttrAddressLine3, # Line three of multiple lines of address data for a user.
-
- for label in self.valuesForAttribute(dsattributes.kDSNAttrPostalAddress):
- addUniqueProperty(vcard, Property("LABEL", label, params={"TYPE": ["POSTAL", "PARCEL",]}), None, dsattributes.kDSNAttrPostalAddress, label)
-
- for label in self.valuesForAttribute(dsattributes.kDSNAttrPostalAddressContacts):
- addUniqueProperty(vcard, Property("LABEL", label, params={"TYPE": ["POSTAL", "PARCEL",]}), None, dsattributes.kDSNAttrPostalAddressContacts, label)
-
- address = self.joinedValuesForAttribute(dsattributes.kDSNAttrAddressLine1)
- addressLine2 = self.joinedValuesForAttribute(dsattributes.kDSNAttrAddressLine2)
- if len(addressLine2) > 0:
- address += "\n" + addressLine2
- addressLine3 = self.joinedValuesForAttribute(dsattributes.kDSNAttrAddressLine3)
- if len(addressLine3) > 0:
- address += "\n" + addressLine3
-
- if len(address) > 0:
- vcard.addProperty(Property("LABEL", address, params={"TYPE": ["POSTAL", "PARCEL",]}))
-
- # 3.3 TELECOMMUNICATIONS ADDRESSING TYPES http://tools.ietf.org/html/rfc2426#section-3.3
- # 3.3.1 TEL Type Definition
- # TEL;TYPE=work,voice,pref,msg:+1-213-555-1234
-
- # dsattributes.kDSNAttrPhoneNumber, # Telephone number of a user.
- # dsattributes.kDSNAttrMobileNumber, # Represents the mobile numbers of a user or person.
- # Usually found in user or people records (kDSStdRecordTypeUsers or
- # dsattributes.kDSStdRecordTypePeople).
- # dsattributes.kDSNAttrFaxNumber, # Represents the FAX numbers of a user or person.
- # Usually found in user or people records (kDSStdRecordTypeUsers or
- # kDSStdRecordTypePeople).
- # dsattributes.kDSNAttrPagerNumber, # Represents the pager numbers of a user or person.
- # Usually found in user or people records (kDSStdRecordTypeUsers or
- # dsattributes.kDSStdRecordTypePeople).
- # dsattributes.kDSNAttrHomePhoneNumber, # Home telephone number of a user or person.
- # dsattributes.kDSNAttrPhoneContacts, # multi-valued attribute that defines a record's custom phone numbers .
- # found in user records (kDSStdRecordTypeUsers).
- # Example: home fax:408-555-4444
-
- params = {"TYPE": ["WORK", "PREF", "VOICE",],}
- for phone in self.valuesForAttribute(dsattributes.kDSNAttrPhoneNumber):
- addUniqueProperty(vcard, Property("TEL", phone, params=params), (("TYPE", "PREF"),), phone, dsattributes.kDSNAttrPhoneNumber)
- params = {"TYPE": ["WORK", "VOICE",],}
-
- params = { "TYPE": ["WORK", "PREF", "CELL",], }
- for phone in self.valuesForAttribute(dsattributes.kDSNAttrMobileNumber):
- addUniqueProperty(vcard, Property("TEL", phone, params=params), (("TYPE", "PREF"),), phone, dsattributes.kDSNAttrMobileNumber)
- params = { "TYPE": ["WORK", "CELL",], }
-
- params = { "TYPE": ["WORK", "PREF", "FAX",], }
- for phone in self.valuesForAttribute(dsattributes.kDSNAttrFaxNumber):
- addUniqueProperty(vcard, Property("TEL", phone, params=params), (("TYPE", "PREF"),), phone, dsattributes.kDSNAttrFaxNumber)
- params = { "TYPE": ["WORK", "FAX",], }
-
- params = { "TYPE": ["WORK", "PREF", "PAGER",], }
- for phone in self.valuesForAttribute(dsattributes.kDSNAttrPagerNumber):
- addUniqueProperty(vcard, Property("TEL", phone, params=params), (("TYPE", "PREF"),), phone, dsattributes.kDSNAttrPagerNumber)
- params = { "TYPE": ["WORK", "PAGER",], }
-
- params = { "TYPE": ["HOME", "PREF", "VOICE",], }
- for phone in self.valuesForAttribute(dsattributes.kDSNAttrHomePhoneNumber):
- addUniqueProperty(vcard, Property("TEL", phone, params=params), (("TYPE", "PREF"),), phone, dsattributes.kDSNAttrHomePhoneNumber)
- params = { "TYPE": ["HOME", "VOICE",], }
-
- addPropertiesAndLabelsForPrefixedAttribute(groupCount=groupCount, propertyPrefix=None, propertyName="TEL", defaultLabel="work",
- nolabelParamTypes=("VOICE", "CELL", "FAX", "PAGER",),
- attrType=dsattributes.kDSNAttrPhoneContacts, )
-
- """
- # EXTEND: Use this attribute
- # dsattributes.kDSNAttrAreaCode, # Area code of a user's phone number.
- """
-
- # 3.3.2 EMAIL Type Definition
- # dsattributes.kDSNAttrEMailAddress, # Email address of usually a user record.
-
- # setup some params
- preferredWorkParams = { "TYPE": ["WORK", "PREF", "INTERNET",], }
- workParams = { "TYPE": ["WORK", "INTERNET",], }
- params = preferredWorkParams
- for emailAddress in self.valuesForAttribute(dsattributes.kDSNAttrEMailAddress):
- addUniqueProperty(vcard, Property("EMAIL", emailAddress, params=params), (("TYPE", "PREF"),), emailAddress, dsattributes.kDSNAttrEMailAddress)
- params = workParams
-
- # dsattributes.kDSNAttrEMailContacts, # multi-valued attribute that defines a record's custom email addresses .
- # found in user records (kDSStdRecordTypeUsers).
- # Example: home:johndoe at mymail.com
-
- # check to see if parameters type are open ended. Could be any string
- addPropertiesAndLabelsForPrefixedAttribute(groupCount=groupCount, propertyPrefix=None, propertyName="EMAIL", defaultLabel="work",
- nolabelParamTypes=("WORK", "HOME",),
- attrType=dsattributes.kDSNAttrEMailContacts, )
-
- """
- # UNIMPLEMENTED:
- # 3.3.3 MAILER Type Definition
- """
- # 3.4 GEOGRAPHICAL TYPES http://tools.ietf.org/html/rfc2426#section-3.4
- """
- # UNIMPLEMENTED:
- # 3.4.1 TZ Type Definition
- """
- # 3.4.2 GEO Type Definition
- #dsattributes.kDSNAttrMapCoordinates, # attribute that defines coordinates for a user's location .
- # Found in user records (kDSStdRecordTypeUsers) and resource records (kDSStdRecordTypeResources).
- # Example: 7.7,10.6
- for coordinate in self.valuesForAttribute(dsattributes.kDSNAttrMapCoordinates):
- parts = coordinate.split(",")
- if (len(parts) == 2):
- vcard.addProperty(Property("GEO", parts))
- else:
- self.log_info("Ignoring malformed attribute %r with value %r. Well-formed example: 7.7,10.6." % (dsattributes.kDSNAttrMapCoordinates, coordinate))
- #
- # 3.5 ORGANIZATIONAL TYPES http://tools.ietf.org/html/rfc2426#section-3.5
- #
- # 3.5.1 TITLE Type Definition
- for jobTitle in self.valuesForAttribute(dsattributes.kDSNAttrJobTitle):
- addUniqueProperty(vcard, Property("TITLE", jobTitle), None, dsattributes.kDSNAttrJobTitle, jobTitle)
-
- """
- # UNIMPLEMENTED:
- # 3.5.2 ROLE Type Definition
- # 3.5.3 LOGO Type Definition
- # 3.5.4 AGENT Type Definition
- """
- # 3.5.5 ORG Type Definition
- company = self.joinedValuesForAttribute(dsattributes.kDSNAttrCompany)
- if len(company) == 0:
- company = self.joinedValuesForAttribute(dsattributes.kDSNAttrOrganizationName)
- department = self.joinedValuesForAttribute(dsattributes.kDSNAttrDepartment)
- extra = self.joinedValuesForAttribute(dsattributes.kDSNAttrOrganizationInfo)
- if len(company) > 0 or len(department) > 0:
- vcard.addProperty(Property("ORG", (company, department, extra, ),))
-
- # 3.6 EXPLANATORY TYPES http://tools.ietf.org/html/rfc2426#section-3.6
- """
- # UNIMPLEMENTED:
- # 3.6.1 CATEGORIES Type Definition
- """
- # 3.6.2 NOTE Type Definition
- # dsattributes.kDS1AttrComment, # Attribute used for unformatted comment.
- # dsattributes.kDS1AttrNote, # Note attribute. Commonly used in printer records.
- for comment in self.valuesForAttribute(dsattributes.kDS1AttrComment):
- addUniqueProperty(vcard, Property("NOTE", comment), None, dsattributes.kDS1AttrComment, comment)
-
- for note in self.valuesForAttribute(dsattributes.kDS1AttrNote):
- addUniqueProperty(vcard, Property("NOTE", note), None, dsattributes.kDS1AttrNote, note)
-
- # 3.6.3 PRODID Type Definition
- #vcard.addProperty(Property("PRODID", vCardProductID + "//BUILD %s" % twistedcaldav.__version__))
- #vcard.addProperty(Property("PRODID", vCardProductID))
- # ADDED WITH CONTSTANT PROPERTIES
-
- # 3.6.4 REV Type Definition
- revDate = self.isoDateStringForDateAttribute(dsattributes.kDS1AttrModificationTimestamp)
- if revDate:
- vcard.addProperty(Property("REV", PyCalendarDateTime.parseText(revDate, fullISO=True)))
-
- """
- # UNIMPLEMENTED:
- # 3.6.5 SORT-STRING Type Definition
- # 3.6.6 SOUND Type Definition
- """
- # 3.6.7 UID Type Definition
- # dsattributes.kDS1AttrGeneratedUID, # Used for 36 character (128 bit) unique ID. Usually found in user,
- # group, and computer records. An example value is "A579E95E-CDFE-4EBC-B7E7-F2158562170F".
- # The standard format contains 32 hex characters and four hyphen characters.
-
- vcard.addProperty(Property("UID", self.firstValueForAttribute(dsattributes.kDS1AttrGeneratedUID)))
-
-
- # 3.6.8 URL Type Definition
- # dsattributes.kDSNAttrURL, # List of URLs.
- # dsattributes.kDS1AttrWeblogURI, # Single-valued attribute that defines the URI of a user's weblog.
- # Usually found in user records (kDSStdRecordTypeUsers).
- # Example: http://example.com/blog/jsmith
- for url in self.valuesForAttribute(dsattributes.kDS1AttrWeblogURI):
- addPropertyAndLabel(groupCount, "weblog", "URL", url, parameters = {"TYPE": ["Weblog",]})
-
- for url in self.valuesForAttribute(dsattributes.kDSNAttrURL):
- addPropertyAndLabel(groupCount, "_$!<HomePage>!$_", "URL", url, parameters = {"TYPE": ["Homepage",]})
-
-
- # 3.6.9 VERSION Type Definition
- # ALREADY ADDED
-
- # 3.7 SECURITY TYPES http://tools.ietf.org/html/rfc2426#section-3.7
- # 3.7.1 CLASS Type Definition
- # ALREADY ADDED
-
- # 3.7.2 KEY Type Definition
-
- # dsattributes.kDSNAttrPGPPublicKey, # Pretty Good Privacy public encryption key.
- # dsattributes.kDS1AttrUserCertificate, # Attribute containing the binary of the user's certificate.
- # Usually found in user records. The certificate is data which identifies a user.
- # This data is attested to by a known party, and can be independently verified
- # by a third party.
- # dsattributes.kDS1AttrUserPKCS12Data, # Attribute containing binary data in PKCS #12 format.
- # Usually found in user records. The value can contain keys, certificates,
- # and other related information and is encrypted with a passphrase.
- # dsattributes.kDS1AttrUserSMIMECertificate,# Attribute containing the binary of the user's SMIME certificate.
- # Usually found in user records. The certificate is data which identifies a user.
- # This data is attested to by a known party, and can be independently verified
- # by a third party. SMIME certificates are often used for signed or encrypted
- # emails.
-
- for key in self.valuesForAttribute(dsattributes.kDSNAttrPGPPublicKey):
- addUniqueProperty(vcard, Property("KEY", key, params = {"ENCODING": ["b",], "TYPE": ["PGPPublicKey",]}), None, dsattributes.kDSNAttrPGPPublicKey, key)
-
- for key in self.valuesForAttribute(dsattributes.kDS1AttrUserCertificate):
- addUniqueProperty(vcard, Property("KEY", key, params = {"ENCODING": ["b",], "TYPE": ["UserCertificate",]}), None, dsattributes.kDS1AttrUserCertificate, key)
-
- for key in self.valuesForAttribute(dsattributes.kDS1AttrUserPKCS12Data):
- addUniqueProperty(vcard, Property("KEY", key, params = {"ENCODING": ["b",], "TYPE": ["UserPKCS12Data",]}), None, dsattributes.kDS1AttrUserPKCS12Data, key)
-
- for key in self.valuesForAttribute(dsattributes.kDS1AttrUserSMIMECertificate):
- addUniqueProperty(vcard, Property("KEY", key, params = {"ENCODING": ["b",], "TYPE": ["UserSMIMECertificate",]}), None, dsattributes.kDS1AttrUserSMIMECertificate, key)
-
- """
- X- attributes, Address Book support
- """
- # X-AIM, X-JABBER, X-MSN, X-YAHOO, X-ICQ
- # instant messaging
- # dsattributes.kDSNAttrIMHandle, # Represents the Instant Messaging handles of a user.
- # Values should be prefixed with the appropriate IM type
- # ie. AIM:, Jabber:, MSN:, Yahoo:, or ICQ:
- # Usually found in user records (kDSStdRecordTypeUsers).
- imNolabelParamTypes=("AIM", "FACEBOOK", "GAGU-GAGU", "GOOGLE TALK", "ICQ", "JABBER", "MSN", "QQ", "SKYPE", "YAHOO",)
- addPropertiesAndLabelsForPrefixedAttribute(groupCount=groupCount, propertyPrefix="X-", propertyName=None, defaultLabel="aim",
- nolabelParamTypes=imNolabelParamTypes,
- attrType=dsattributes.kDSNAttrIMHandle,)
-
-
-
- # IMPP
- # Address Book's implementation of http://tools.ietf.org/html/rfc6350#section-6.4.3
- # adding IMPP property allows ab query report search on one property
- addPropertiesAndLabelsForPrefixedAttribute(groupCount=groupCount, propertyPrefix=None, propertyName="IMPP", defaultLabel="aim",
- specialParamType = "X-SERVICE-TYPE",
- nolabelParamTypes=imNolabelParamTypes,
- attrType=dsattributes.kDSNAttrIMHandle,)
-
- # X-ABRELATEDNAMES
- # dsattributes.kDSNAttrRelationships, # multi-valued attribute that defines the relationship to the record type .
- # found in user records (kDSStdRecordTypeUsers).
- # Example: brother:John
- addPropertiesAndLabelsForPrefixedAttribute(groupCount=groupCount, propertyPrefix=None, propertyName="X-ABRELATEDNAMES", defaultLabel="friend",
- labelMap={ "FATHER":"_$!<Father>!$_",
- "MOTHER":"_$!<Mother>!$_",
- "PARENT":"_$!<Parent>!$_",
- "BROTHER":"_$!<Brother>!$_",
- "SISTER":"_$!<Sister>!$_",
- "CHILD":"_$!<Child>!$_",
- "FRIEND":"_$!<Friend>!$_",
- "SPOUSE":"_$!<Spouse>!$_",
- "PARTNER":"_$!<Partner>!$_",
- "ASSISTANT":"_$!<Assistant>!$_",
- "MANAGER":"_$!<Manager>!$_", },
- attrType=dsattributes.kDSNAttrRelationships, )
-
-
- # special case for Apple
- if self.appleInternalServer:
- for manager in self.valuesForAttribute("dsAttrTypeNative:appleManager"):
- splitManager = manager.split("|")
- if len(splitManager) >= 4:
- managerValue = "%s %s, %s" % (splitManager[0], splitManager[1], splitManager[3],)
- elif len(splitManager) >= 2:
- managerValue = "%s %s" % (splitManager[0], splitManager[1])
- else:
- managerValue = manager
- addPropertyAndLabel( groupCount, "_$!<Manager>!$_", "X-ABRELATEDNAMES", managerValue, parameters={ "TYPE": ["Manager",]} )
-
- """
- # UNIMPLEMENTED: X- attributes
-
- X-MAIDENNAME
- X-PHONETIC-FIRST-NAME
- X-PHONETIC-MIDDLE-NAME
- X-PHONETIC-LAST-NAME
-
- sattributes.kDS1AttrPicture, # Represents the path of the picture for each user displayed in the login window.
- # Found in user records (kDSStdRecordTypeUsers).
-
- dsattributes.kDS1AttrMapGUID, # Represents the GUID for a record's map.
- dsattributes.kDSNAttrMapURI, # attribute that defines the URI of a user's location.
-
- dsattributes.kDSNAttrOrganizationInfo, # Usually the organization info of a user.
- dsattributes.kDSNAttrAreaCode, # Area code of a user's phone number.
-
- dsattributes.kDSNAttrMIME, # Data contained in this attribute type is a fully qualified MIME Type.
-
- """
-
- # debug, create x attributes for all ds attributes
- if self.addDSAttrXProperties:
- for attribute in self.originalAttributes:
- for value in self.valuesForAttribute(attribute):
- vcard.addProperty(Property("X-"+"-".join(attribute.split(":")), removeControlChars(value)))
-
- return vcard
-
-
- if not self._vCard:
- self._vCard = generateVCard()
-
- return self._vCard
-
- def vCardText(self):
- return str(self.vCard())
-
- def uriName(self):
- return self.vCard().getProperty("UID").value() + ".vcf"
-
- def hRef(self, parentURI="/directory/"):
- # FIXME: Get the parent URI from self._directoryBackedAddressBook
- return davxml.HRef.fromString(joinURL(parentURI, self.uriName()))
-
-
- def readProperty(self, property, request):
-
- if type(property) is tuple:
- qname = property
- else:
- qname = property.qname()
-
- namespace, name = qname
-
- #print("ABDirectoryQueryResult.readProperty: qname = %s" % (qname, ))
-
- if namespace == dav_namespace:
- if name == "resourcetype":
- result = davxml.ResourceType.empty #@UndefinedVariable
- #print("ABDirectoryQueryResult.readProperty: qname = %s, result = %s" % (qname, result))
- return result
- elif name == "getetag":
- result = davxml.GETETag( ETag(hashlib.md5(self.vCardText()).hexdigest()).generate() )
- #print("ABDirectoryQueryResult.readProperty: qname = %s, result = %s" % (qname, result))
- return result
- elif name == "getcontenttype":
- mimeType = MimeType('text', 'vcard', {})
- result = davxml.GETContentType(generateContentType(mimeType))
- #print("ABDirectoryQueryResult.readProperty: qname = %s, result = %s" % (qname, result))
- return result
- elif name == "getcontentlength":
- result = davxml.GETContentLength.fromString(str(len(self.vCardText())))
- #print("ABDirectoryQueryResult.readProperty: qname = %s, result = %s" % (qname, result))
- return result
- elif name == "getlastmodified":
- if self.vCard().hasProperty("REV"):
- modDatetime = parse_date(self.vCard().propertyValue("REV"))
- else:
- # use creation date attribute if it exists
- creationDateString = self.isoDateStringForDateAttribute(dsattributes.kDS1AttrCreationTimestamp)
- if creationDateString:
- modDatetime = parse_date(creationDateString)
- else:
- modDatetime = datetime.datetime.utcnow()
-
- #strip time zone because time zones are unimplemented in davxml.GETLastModified.fromDate
- d = modDatetime.date()
- t = modDatetime.time()
- modDatetimeNoTZ = datetime.datetime(d.year, d.month, d.day, t.hour, t.minute, t.second, t.microsecond, None)
- result = davxml.GETLastModified.fromDate(modDatetimeNoTZ)
- #print("ABDirectoryQueryResult.readProperty: qname = %s, result = %s" % (qname, result))
- return result
- elif name == "creationdate":
- creationDateString = self.isoDateStringForDateAttribute(dsattributes.kDS1AttrCreationTimestamp)
- if creationDateString:
- creationDatetime = parse_date(creationDateString)
- elif self.vCard().hasProperty("REV"): # use modification date property if it exists
- creationDatetime = parse_date(self.vCard().propertyValue("REV"))
- else:
- creationDatetime = datetime.datetime.utcnow()
- result = davxml.CreationDate.fromDate(creationDatetime)
- #print("ABDirectoryQueryResult.readProperty: qname = %s, result = %s" % (qname, result))
- return result
- elif name == "displayname":
- # AddressBook.app uses N. Use FN or UID instead?
- result = davxml.DisplayName.fromString(self.vCard().propertyValue("N"))
- #print("ABDirectoryQueryResult.readProperty: qname = %s, result = %s" % (qname, result))
- return result
-
- elif namespace == twisted_dav_namespace:
- return super(ABDirectoryQueryResult, self).readProperty(property, request)
- #return DAVPropertyMixIn.readProperty(self, property, request)
-
- return self._directoryBackedAddressBook.readProperty(property, request)
-
- def listProperties(self, request):
- #print("ABDirectoryQueryResult.listProperties()")
- qnames = set(self.liveProperties())
-
- # Add dynamic live properties that exist
- dynamicLiveProperties = (
- (dav_namespace, "quota-available-bytes" ),
- (dav_namespace, "quota-used-bytes" ),
- )
- for dqname in dynamicLiveProperties:
- #print("ABDirectoryQueryResult.listProperties: removing dqname=%s" % (dqname,))
- qnames.remove(dqname)
-
- for qname in self.deadProperties().list():
- if (qname not in qnames) and (qname[0] != twisted_private_namespace):
- #print("listProperties: adding qname=%s" % (qname,))
- qnames.add(qname)
-
- #for qn in qnames: print("ABDirectoryQueryResult.listProperties: qn=%s" % (qn,))
-
- yield qnames
-
- listProperties = deferredGenerator(listProperties)
-
-# utility
-#remove illegal XML
-def removeControlChars( utf8String ):
- result = ''.join([c for c in utf8String if c not in "\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"])
- return result
-
-
Copied: CalendarServer/branches/users/gaya/ldapdirectorybacker/twistedcaldav/directory/opendirectorybacker.py (from rev 8981, CalendarServer/branches/users/gaya/ldapdirectorybacker/twistedcaldav/directory/opendirectorybacker.py)
===================================================================
--- CalendarServer/branches/users/gaya/ldapdirectorybacker/twistedcaldav/directory/opendirectorybacker.py (rev 0)
+++ CalendarServer/branches/users/gaya/ldapdirectorybacker/twistedcaldav/directory/opendirectorybacker.py 2012-04-06 19:47:21 UTC (rev 8994)
@@ -0,0 +1,1727 @@
+##
+# Copyright (c) 2006-2012 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+
+"""
+Apple Open Directory directory service implementation for backing up directory-backed address books
+"""
+
+__all__ = [
+ "OpenDirectoryBackingService", "ABDirectoryQueryResult",
+]
+
+import traceback
+import hashlib
+
+import os
+import sys
+import time
+
+from os.path import join
+from random import random
+
+from pycalendar.n import N
+from pycalendar.adr import Adr
+from pycalendar.datetime import PyCalendarDateTime
+
+from socket import getfqdn
+
+from twisted.internet.defer import inlineCallbacks, returnValue, deferredGenerator, succeed
+from txdav.xml import element as davxml
+from txdav.xml.base import twisted_dav_namespace, dav_namespace, parse_date, twisted_private_namespace
+from twext.web2.dav.resource import DAVPropertyMixIn
+from twext.web2.dav.util import joinURL
+from twext.web2.http_headers import MimeType, generateContentType, ETag
+
+
+from twistedcaldav import carddavxml
+from twistedcaldav.config import config
+from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
+from twistedcaldav.query import addressbookqueryfilter
+from twistedcaldav.vcard import Component, Property, vCardProductID
+
+from xmlrpclib import datetime
+
+from calendarserver.platform.darwin.od import dsattributes, dsquery
+from twisted.python.reflect import namedModule
+
+from twext.python.log import LoggingMixIn
+
+class OpenDirectoryBackingService(DirectoryService):
+ """
+ Open Directory implementation of L{IDirectoryService}.
+ """
+
+ baseGUID = "BF07A1A2-5BB5-4A4D-A59A-67260EA7E143"
+
+ def __repr__(self):
+ return "<%s %r>" % (self.__class__.__name__, self.realmName, )
+
+ def __init__(self, params):
+ self._actuallyConfigure(**params)
+
+ def _actuallyConfigure(
+ self, queryPeopleRecords=True,
+ peopleNode = "/Search/Contacts",
+ queryUserRecords=True,
+ userNode = "/Search",
+ maxDSQueryRecords = 0, # maximum number of records requested for any ds query
+
+ queryDSLocal = False, #query in DSLocal -- debug
+ dsLocalCacheTimeout = 30,
+ ignoreSystemRecords = True,
+
+ fakeETag = True, # eTag is not reliable if True
+
+ addDSAttrXProperties=False, # add dsattributes to vcards as "X-" attributes
+ generateSimpleUIDs = False, # use simple synthetic UIDs --- good for testing
+ appleInternalServer=False,
+
+ additionalAttributes=[],
+ allowedAttributes=[],
+ directoryBackedAddressBook=None
+ ):
+ """
+ @queryPeopleRecords: C{True} to query for People records
+ @queryUserRecords: C{True} to query for User records
+ @maxDSQueryRecords: maximum number of (unfiltered) ds records retrieved before raising
+ NumberOfMatchesWithinLimits exception or returning results
+ @dsLocalCacheTimeout: how log to keep cache of DSLocal records
+ @fakeETag: C{True} to use a fake eTag; allows ds queries with partial attributes
+ @generateSimpleUIDs: C{True} when creating synthetic UID (==f(Node, Type, Record Name)),
+ use a standard Node name. This allows testing with the same UID on different hosts
+ @allowedAttributes: list of DSAttributes that are used to create VCards
+
+ """
+ assert directoryBackedAddressBook is not None
+ self.directoryBackedAddressBook = directoryBackedAddressBook
+
+ self.peopleDirectory = None
+ self.peopleNode = None
+ self.userDirectory = None
+ self.userNode = None
+
+ self.realmName = None # needed for super
+
+ self.odModule = namedModule(config.OpenDirectoryModule)
+
+ if queryPeopleRecords or not queryUserRecords:
+ self.peopleNode = peopleNode
+ try:
+ self.peopleDirectory = self.odModule.odInit(peopleNode)
+ except self.odModule.ODError, e:
+ self.log_error("Open Directory (node=%s) Initialization error: %s" % (peopleNode, e))
+ raise
+ self.realmName = peopleNode
+
+ if queryUserRecords:
+ if self.peopleNode == userNode: # use sane directory and node if they are equal
+ self.userNode = self.peopleNode
+ self.userDirectory = self.peopleDirectory
+ else:
+ self.userNode = userNode
+ try:
+ self.userDirectory = self.odModule.odInit(userNode)
+ except self.odModule.ODError, e:
+ self.log_error("Open Directory (node=%s) Initialization error: %s" % (userNode, e))
+ raise
+ if self.realmName:
+ self.realmName += "+" + userNode
+ else:
+ self.realmName = userNode
+
+
+ self.maxDSQueryRecords = maxDSQueryRecords
+
+ self.ignoreSystemRecords = ignoreSystemRecords
+ self.queryDSLocal = queryDSLocal
+ self.dsLocalCacheTimeout = dsLocalCacheTimeout
+
+ self.fakeETag = fakeETag
+
+ self.addDSAttrXProperties = addDSAttrXProperties
+ self.generateSimpleUIDs = generateSimpleUIDs
+ self.appleInternalServer = appleInternalServer
+
+ if ignoreSystemRecords:
+ # needed for sytem record test, but not has no part in generating in vCard properties
+ additionalAttributes += [dsattributes.kDS1AttrUniqueID,]
+
+ self.additionalAttributes = additionalAttributes
+ # filter allows attributes, but make sure there are a minimum of attributes for functionality
+ if allowedAttributes:
+ self.allowedDSQueryAttributes = sorted(list(set(
+ [attr for attr in ABDirectoryQueryResult.allDSQueryAttributes
+ if (isinstance(attr, str) and attr in allowedAttributes) or
+ (isinstance(attr, tuple) and attr[0] in allowedAttributes)] +
+ ABDirectoryQueryResult.dsqueryAttributesForProperty.get("X-INTERNAL-REQUIRED")
+ )))
+ if (self.allowedDSQueryAttributes != ABDirectoryQueryResult.allDSQueryAttributes):
+ self.log_info("Allowed DS query attributes = %r" % (self.allowedDSQueryAttributes, ))
+ else:
+ self.allowedDSQueryAttributes = ABDirectoryQueryResult.allDSQueryAttributes
+
+ #self.returnedAttributes = ABDirectoryQueryResult.allDSQueryAttributes
+ self.returnedAttributes = self.allowedDSQueryAttributes
+
+
+
+
+ self._dsLocalResults = {}
+ self._nextDSLocalQueryTime = 0
+
+ def __cmp__(self, other):
+ if not isinstance(other, DirectoryRecord):
+ return super(DirectoryRecord, self).__eq__(other)
+
+ for attr in ("directory", "node"):
+ diff = cmp(getattr(self, attr), getattr(other, attr))
+ if diff != 0:
+ return diff
+ return 0
+
+ def __hash__(self):
+ h = hash(self.__class__.__name__)
+ for attr in ("node",):
+ h = (h + hash(getattr(self, attr))) & sys.maxint
+ return h
+
+ def createCache(self):
+ succeed(None)
+
+ def _isSystemRecord(self, recordShortName, recordAttributes):
+
+ guid = recordAttributes.get(dsattributes.kDS1AttrGeneratedUID)
+ if guid and guid.startswith("FFFFEEEE-DDDD-CCCC-BBBB-AAAA"):
+ self.log_info("Ignoring system record %s with %s %s" % (recordShortName, dsattributes.kDS1AttrGeneratedUID, guid,))
+ return True
+
+ uniqueID = recordAttributes.get(dsattributes.kDS1AttrUniqueID)
+ if uniqueID and (int(uniqueID) < 500 or int(uniqueID) == 1000):
+ self.log_info("Ignoring system record %s with %s %s" % (recordShortName, dsattributes.kDS1AttrUniqueID, uniqueID,))
+ return True
+
+ return False
+
+
+ def _getAllDSLocalResults(self):
+
+ def generateDSLocalResults():
+
+ resultsDictionary = {}
+
+ recordTypes = [dsattributes.kDSStdRecordTypePeople, dsattributes.kDSStdRecordTypeUsers, ]
+ try:
+ localNodeDirectory = self.odModule.odInit("/Local/Default")
+ returnedAttributes = list(set(self.returnedAttributes + self.additionalAttributes))
+ self.log_debug("opendirectory.listAllRecordsWithAttributes_list(%r,%r,%r)" % (
+ "/DSLocal",
+ recordTypes,
+ returnedAttributes,
+ ))
+ records = list(self.odModule.listAllRecordsWithAttributes_list(
+ localNodeDirectory,
+ recordTypes,
+ returnedAttributes,
+ ))
+ except self.odModule.ODError, ex:
+ self.log_error("Open Directory (node=%s) error: %s" % ("/Local/Default", str(ex)))
+ raise
+
+ for (recordShortName, recordAttributes) in records: #@UnusedVariable
+
+ try:
+ self.log_info("Inspecting record %s" % (recordAttributes,))
+ if self.ignoreSystemRecords:
+ if self._isSystemRecord(recordShortName, recordAttributes):
+ continue
+
+ result = ABDirectoryQueryResult(self.directoryBackedAddressBook, recordAttributes)
+
+ except:
+ traceback.print_exc()
+ self.log_info("Could not get vcard for record %s" % (recordShortName,))
+
+ else:
+ uid = result.vCard().propertyValue("UID")
+
+ if uid in resultsDictionary:
+ self.log_info("Record %s skipped due to duplicate UID: %s" % (recordShortName, uid,))
+ continue
+
+ self.log_debug("VCard text =\n%s" % (result.vCardText(), ))
+ resultsDictionary[uid] = result
+
+
+ return resultsDictionary
+
+
+ if not self.queryDSLocal:
+ return {}
+
+ if time.time() > self._nextDSLocalQueryTime:
+ self._dsLocalResults = generateDSLocalResults()
+ # Add jitter/fuzz factor
+ self._nextDSLocalQueryTime = time.time() + self.dsLocalCacheTimeout * (random() + 0.5) * 60
+
+ return self._dsLocalResults
+
+
+ @inlineCallbacks
+ def _getDirectoryQueryResults(self, query=None, attributes=None, maxRecords=0 ):
+ """
+ Get a list of filtered ABDirectoryQueryResult for the given query with the given attributes.
+ query == None gets all records. attribute == None gets ABDirectoryQueryResult.allDSQueryAttributes
+ """
+ limited = False
+ records = (yield self._queryDirectory(query, attributes, maxRecords ))
+ if maxRecords and len(records) >= maxRecords:
+ limited = True
+ self.log_debug("Directory address book record limit (= %d) reached." % (maxRecords, ))
+
+ self.log_debug("Query done. Inspecting %s records" % len(records))
+
+ resultsDictionary = self._getAllDSLocalResults().copy()
+ self.log_debug("Adding %s DSLocal results" % len(resultsDictionary.keys()))
+
+ for (recordShortName, recordAttributes) in records: #@UnusedVariable
+
+ try:
+ # fix ds strangeness
+ if recordAttributes.get(dsattributes.kDS1AttrLastName, "") == "99":
+ del recordAttributes[dsattributes.kDS1AttrLastName]
+
+ if self.ignoreSystemRecords:
+ if self._isSystemRecord(recordShortName, recordAttributes):
+ continue
+
+ result = ABDirectoryQueryResult(self.directoryBackedAddressBook, recordAttributes,
+ generateSimpleUIDs=self.generateSimpleUIDs,
+ addDSAttrXProperties=self.addDSAttrXProperties,
+ appleInternalServer=self.appleInternalServer,
+ )
+ except:
+ traceback.print_exc()
+ self.log_info("Could not get vcard for record %s" % (recordShortName,))
+
+ else:
+ uid = result.vCard().propertyValue("UID")
+
+ if uid in resultsDictionary:
+ self.log_info("Record skipped due to duplicate UID: %s" % (recordShortName,))
+ continue
+
+ self.log_debug("VCard text =\n%s" % (result.vCardText(), ))
+ resultsDictionary[uid] = result
+
+ self.log_debug("After filtering, %s results (limited=%s)." % (len(resultsDictionary), limited))
+ returnValue((resultsDictionary.values(), limited, ))
+
+
+ def _queryDirectory(self, query=None, attributes=None, maxRecords=0 ):
+
+ startTime = time.time()
+
+
+ if not attributes:
+ attributes = self.returnedAttributes
+
+ attributes = list(set(attributes + self.additionalAttributes)) # remove duplicates
+
+ directoryAndRecordTypes = []
+ if self.peopleDirectory == self.userDirectory:
+ # use single ds query if possible for best performance
+ directoryAndRecordTypes.append( (self.peopleDirectory, self.peopleNode, (dsattributes.kDSStdRecordTypePeople, dsattributes.kDSStdRecordTypeUsers) ) )
+ else:
+ if self.peopleDirectory:
+ directoryAndRecordTypes.append( (self.peopleDirectory, self.peopleNode, dsattributes.kDSStdRecordTypePeople) )
+ if self.userDirectory:
+ directoryAndRecordTypes.append( (self.userDirectory, self.userNode, dsattributes.kDSStdRecordTypeUsers) )
+
+ allResults = []
+ for directory, node, recordType in directoryAndRecordTypes:
+ try:
+ if query:
+ if isinstance(query, dsquery.match) and query.value is not "":
+ self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r,%r)" % (
+ node,
+ query.attribute,
+ query.value,
+ query.matchType,
+ False,
+ recordType,
+ attributes,
+ maxRecords,
+ ))
+ results = list(
+ self.odModule.queryRecordsWithAttribute_list(
+ directory,
+ query.attribute,
+ query.value,
+ query.matchType,
+ False,
+ recordType,
+ attributes,
+ maxRecords,
+ ))
+ else:
+ self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r)" % (
+ node,
+ query.generate(),
+ False,
+ recordType,
+ attributes,
+ maxRecords,
+ ))
+ results = list(
+ self.odModule.queryRecordsWithAttributes_list(
+ directory,
+ query.generate(),
+ False,
+ recordType,
+ attributes,
+ maxRecords,
+ ))
+ else:
+ self.log_debug("opendirectory.listAllRecordsWithAttributes_list(%r,%r,%r,%r)" % (
+ node,
+ recordType,
+ attributes,
+ maxRecords,
+ ))
+ results = list(
+ self.odModule.listAllRecordsWithAttributes_list(
+ directory,
+ recordType,
+ attributes,
+ maxRecords,
+ ))
+ except self.odModule.ODError, ex:
+ self.log_error("Open Directory (node=%s) error: %s" % (self.realmName, str(ex)))
+ raise
+
+ allResults.extend(results)
+
+ if maxRecords:
+ maxRecords -= len(results)
+ if maxRecords <= 0:
+ break
+
+
+ elaspedTime = time.time()-startTime
+ self.log_info("Timing: Directory query: %.1f ms (%d records, %.2f records/sec)" % (elaspedTime*1000, len(allResults), len(allResults)/elaspedTime))
+ return succeed(allResults)
+
+ def _attributesForAddressBookQuery(self, addressBookQuery ):
+
+ etagRequested, propertyNames = propertiesInAddressBookQuery( addressBookQuery )
+
+ if etagRequested and not self.fakeETag:
+ propertyNames = None
+
+ if not propertyNames:
+ #print("using all attributes")
+ return self.returnedAttributes
+
+ else:
+ propertyNames.append("X-INTERNAL-MINIMUM-VCARD-PROPERTIES") # these properties are required to make a vCard
+ queryAttributes = []
+ for prop in propertyNames:
+ if prop in ABDirectoryQueryResult.dsqueryAttributesForProperty:
+ #print("adding attributes %r" % ABDirectoryQueryResult.dsqueryAttributesForProperty.get(prop))
+ queryAttributes += ABDirectoryQueryResult.dsqueryAttributesForProperty.get(prop)
+
+ return list(set(queryAttributes).intersection(set(self.returnedAttributes)))
+
+
+
+ @inlineCallbacks
+ def doAddressBookQuery(self, addressBookFilter, addressBookQuery, maxResults ):
+ """
+ Get vCards for a given addressBookFilter and addressBookQuery
+ """
+
+ allRecords, filterAttributes, dsFilter = dsFilterFromAddressBookFilter( addressBookFilter, vcardPropToDSAttrMap=ABDirectoryQueryResult.vcardPropToDSAttrMap, allowedAttributes=self.allowedDSQueryAttributes );
+ #print("allRecords = %s, query = %s" % (allRecords, "None" if dsFilter is None else dsFilter.generate(),))
+
+ # testing:
+ # allRecords = True
+
+ if allRecords:
+ dsFilter = None # None expression == all Records
+ clear = not allRecords and not dsFilter
+
+ results = []
+ limited = False
+
+ if not clear:
+
+ # add filter to ignore system records rather than post filtering
+ # but this appears to be broken in open directory
+ '''
+ if self.ignoreSystemRecords:
+ ignoreExpression = dsquery.expression( dsquery.expression.NOT,
+ dsquery.match(dsattributes.kDS1AttrGeneratedUID, "FFFFEEEE-DDDD-CCCC-BBBB-AAAA", dsattributes.eDSStartsWith)
+ )
+ filterAttributes = list(set(filterAttributes).union(dsattributes.kDS1AttrGeneratedUID))
+
+ dsFilter = dsquery.expression( dsquery.expression.AND, (dsFilter, ignoreExpression,) ) if dsFilter else ignoreExpression
+ #dsFilter = ignoreExpression
+ '''
+
+ queryAttributes = self._attributesForAddressBookQuery( addressBookQuery )
+ attributes = filterAttributes + queryAttributes
+
+ #calc maxRecords from passed in maxResults allowing extra for second stage filtering in caller
+ maxRecords = int(maxResults * 1.2)
+ if self.maxDSQueryRecords and maxRecords > self.maxDSQueryRecords:
+ maxRecords = self.maxDSQueryRecords
+
+ results, limited = (yield self._getDirectoryQueryResults(dsFilter, attributes, maxRecords))
+
+ returnValue((results, limited,))
+
+
+#utility
+def propertiesInAddressBookQuery( addressBookQuery ):
+
+ etagRequested = False
+ propertyNames = []
+ #print( "addressBookQuery.qname=%r" % addressBookQuery.qname)
+ if addressBookQuery.qname() == ("DAV:", "prop"):
+
+ for property in addressBookQuery.children:
+ #print("property = %r" % property )
+ if isinstance(property, carddavxml.AddressData):
+ for addressProperty in property.children:
+ #print("addressProperty = %r" % addressProperty )
+ if isinstance(addressProperty, carddavxml.Property):
+ #print("Adding property %r", addressProperty.attributes["name"])
+ propertyNames.append(addressProperty.attributes["name"])
+
+ elif property.qname() == ("DAV:", "getetag"):
+ # for a real etag == md5(vCard), we need all attributes
+ etagRequested = True
+
+ return (etagRequested, propertyNames if len(propertyNames) else None)
+
+
+def dsFilterFromAddressBookFilter(addressBookFilter, vcardPropToDSAttrMap, allowedAttributes=None):
+ """
+ Convert the supplied addressbook-query into a ds expression tree.
+
+ @param filter: the L{Filter} for the addressbook-query to convert.
+ @return: (needsAllRecords, espressionAttributes, expression) tuple
+ """
+ def propFilterListQuery(filterAllOf, propFilters):
+
+ def propFilterExpression(filterAllOf, propFilter):
+ #print("propFilterExpression")
+ """
+ Create an expression for a single prop-filter element.
+
+ @param propFilter: the L{PropertyFilter} element.
+ @return: (needsAllRecords, espressionAttributes, expressions) tuple
+ """
+
+ def definedExpression( defined, allOf, filterName, constant, queryAttributes, allAttrStrings):
+ if constant or filterName in ("N" , "FN", "UID", ):
+ return (defined, [], []) # all records have this property so no records do not have it
+ else:
+ matchList = list(set([dsquery.match(attrName, "", dsattributes.eDSStartsWith) for attrName in allAttrStrings]))
+ if defined:
+ # TODO: Investigate what happens when andOrExpresion() does not return an expression
+ return andOrExpression(allOf, queryAttributes, matchList)
+ else:
+ if len(matchList) > 1:
+ expr = dsquery.expression( dsquery.expression.OR, matchList )
+ else:
+ expr = matchList[0]
+ return (False, queryAttributes, [dsquery.expression( dsquery.expression.NOT, expr),])
+ #end isNotDefinedExpression()
+
+
+ def andOrExpression(propFilterAllOf, queryAttributes, matchList):
+ #print("andOrExpression(propFilterAllOf=%r, queryAttributes%r, matchList%r)" % (propFilterAllOf, queryAttributes, matchList))
+ if propFilterAllOf and len(matchList) > 1:
+ # add OR expression because parent will AND
+ return (False, queryAttributes, [dsquery.expression( dsquery.expression.OR, matchList),])
+ else:
+ return (False, queryAttributes, matchList)
+ #end andOrExpression()
+
+
+ # short circuit parameter filters
+ def supportedParamter( filterName, paramFilters, propFilterAllOf ):
+
+ def supported( paramFilterName, paramFilterDefined, params ):
+ paramFilterName = paramFilterName.upper()
+ if len(params.keys()) and ((paramFilterName in params.keys()) != paramFilterDefined):
+ return False
+ if len(params[paramFilterName]) and str(paramFilter.qualifier).upper() not in params[paramFilterName]:
+ return False
+ return True
+ #end supported()
+
+
+ oneSupported = False
+ for paramFilter in paramFilters:
+ if filterName == "PHOTO":
+ if propFilterAllOf != supported( paramFilter.filter_name, paramFilter.defined, { "ENCODING": ["B",], "TYPE": ["JPEG",], }):
+ return not propFilterAllOf
+ oneSupported |= propFilterAllOf
+ elif filterName == "ADR":
+ if propFilterAllOf != supported( paramFilter.filter_name, paramFilter.defined, { "TYPE": ["WORK", "PREF", "POSTAL", "PARCEL",], }):
+ return not propFilterAllOf
+ oneSupported |= propFilterAllOf
+ elif filterName == "LABEL":
+ if propFilterAllOf != supported( paramFilter.filter_name, paramFilter.defined, { "TYPE": ["POSTAL", "PARCEL",]}):
+ return not propFilterAllOf
+ oneSupported |= propFilterAllOf
+ elif filterName == "TEL":
+ if propFilterAllOf != supported( paramFilter.filter_name, paramFilter.defined, { "TYPE": [], }): # has params derived from ds attributes
+ return not propFilterAllOf
+ oneSupported |= propFilterAllOf
+ elif filterName == "EMAIL":
+ if propFilterAllOf != supported( paramFilter.filter_name, paramFilter.defined, { "TYPE": [], }): # has params derived from ds attributes
+ return not propFilterAllOf
+ oneSupported |= propFilterAllOf
+ elif filterName == "URL":
+ if propFilterAllOf != supported( paramFilter.filter_name, paramFilter.defined, {}):
+ return not propFilterAllOf
+ oneSupported |= propFilterAllOf
+ elif filterName == "KEY":
+ if propFilterAllOf != supported( paramFilter.filter_name, paramFilter.defined, { "ENCODING": ["B",], "TYPE": ["PGPPUBILICKEY", "USERCERTIFICATE", "USERPKCS12DATA", "USERSMIMECERTIFICATE",] }):
+ return not propFilterAllOf
+ oneSupported |= propFilterAllOf
+ elif not filterName.startswith("X-"): #X- IMHandles X-ABRELATEDNAMES excepted, no other params are used
+ if propFilterAllOf == paramFilter.defined:
+ return not propFilterAllOf
+ oneSupported |= propFilterAllOf
+
+ if propFilterAllOf:
+ return True
+ else:
+ return oneSupported
+ #end supportedParamter()
+
+
+ def textMatchElementExpression( propFilterAllOf, textMatchElement ):
+
+ # pre process text match strings for ds query
+ def getMatchStrings( propFilter, matchString ):
+
+ if propFilter.filter_name in ("REV" , "BDAY", ):
+ rawString = matchString
+ matchString = ""
+ for c in rawString:
+ if not c in "TZ-:":
+ matchString += c
+ elif propFilter.filter_name == "GEO":
+ matchString = ",".join(matchString.split(";"))
+
+ if propFilter.filter_name in ("N" , "ADR", "ORG", ):
+ # for structured properties, change into multiple strings for ds query
+ if propFilter.filter_name == "ADR":
+ #split by newline and comma
+ rawStrings = ",".join( matchString.split("\n") ).split(",")
+ else:
+ #split by space
+ rawStrings = matchString.split(" ")
+
+ # remove empty strings
+ matchStrings = []
+ for oneString in rawStrings:
+ if len(oneString):
+ matchStrings += [oneString,]
+ return matchStrings
+
+ elif len(matchString):
+ return [matchString,]
+ else:
+ return []
+ # end getMatchStrings
+
+ if constant:
+ # do the match right now! Return either all or none.
+ #FIXME: match is not implemented in twisteddaldav.query.addressbookqueryfilter.TextMatch so use _match for now
+ return( textMatchElement._match([constant,]), [], [] )
+ else:
+
+ matchStrings = getMatchStrings(propFilter, textMatchElement.text)
+
+ if not len(matchStrings) or binaryAttrNames:
+ # no searching text in binary ds attributes, so change to defined/not defined case
+ if textMatchElement.negate:
+ return definedExpression(False, propFilterAllOf, propFilter.filter_name, constant, queryAttributes, allAttrStrings)
+ # else fall through to attribute exists case below
+ else:
+
+ # special case UID's formed from node and record name
+ if propFilter.filter_name == "UID":
+ matchString = matchStrings[0]
+ seperatorIndex = matchString.find(ABDirectoryQueryResult.peopleUIDSeparator)
+ if seperatorIndex > 1:
+ recordNameStart = seperatorIndex + len(ABDirectoryQueryResult.peopleUIDSeparator)
+ else:
+ seperatorIndex = matchString.find(ABDirectoryQueryResult.userUIDSeparator)
+ if seperatorIndex > 1:
+ recordNameStart = seperatorIndex + len(ABDirectoryQueryResult.userUIDSeparator)
+ else:
+ recordNameStart = sys.maxint
+
+ if recordNameStart < len(matchString)-1:
+ try:
+ recordNameQualifier = matchString[recordNameStart:].decode("base64").decode("utf8")
+ except Exception, e:
+ self.log_debug("Could not decode UID string %r in %r: %r" % (matchString[recordNameStart:], matchString, e,))
+ else:
+ if textMatchElement.negate:
+ return (False, queryAttributes,
+ [dsquery.expression(dsquery.expression.NOT, dsquery.match(dsattributes.kDSNAttrRecordName, recordNameQualifier, dsattributes.eDSExact)),]
+ )
+ else:
+ return (False, queryAttributes,
+ [dsquery.match(dsattributes.kDSNAttrRecordName, recordNameQualifier, dsattributes.eDSExact),]
+ )
+
+ # use match_type where possible depending on property/attribute mapping
+ # Note that case sensitive negate will not work
+ # Should return all records in that case
+ matchType = dsattributes.eDSContains
+ if propFilter.filter_name in ("NICKNAME" , "TITLE" , "NOTE" , "UID", "URL", "N", "ADR", "ORG", "REV", "LABEL", ):
+ if textMatchElement.match_type == "equals":
+ matchType = dsattributes.eDSExact
+ elif textMatchElement.match_type == "starts-with":
+ matchType = dsattributes.eDSStartsWith
+ elif textMatchElement.match_type == "ends-with":
+ matchType = dsattributes.eDSEndsWith
+
+ matchList = []
+ for matchString in matchStrings:
+ matchList += [dsquery.match(attrName, matchString, matchType) for attrName in stringAttrNames]
+
+ matchList = list(set(matchList))
+
+ if textMatchElement.negate:
+ if len(matchList) > 1:
+ expr = dsquery.expression( dsquery.expression.OR, matchList )
+ else:
+ expr = matchList[0]
+ return (False, queryAttributes, [dsquery.expression( dsquery.expression.NOT, expr),])
+ else:
+ return andOrExpression(propFilterAllOf, queryAttributes, matchList)
+
+ # attribute exists search
+ return definedExpression(True, propFilterAllOf, propFilter.filter_name, constant, queryAttributes, allAttrStrings)
+ #end textMatchElementExpression()
+
+
+ # get attribute strings
+ queryAttributes = vcardPropToDSAttrMap.get(propFilter.filter_name, [])
+ if isinstance(queryAttributes, str):
+ queryAttributes = [queryAttributes,]
+ if allowedAttributes:
+ queryAttributes = list(set(queryAttributes).intersection(set(allowedAttributes)))
+
+ binaryAttrNames = []
+ stringAttrNames = []
+ for attr in queryAttributes:
+ if isinstance(attr, tuple):
+ binaryAttrNames.append(attr[0])
+ else:
+ stringAttrNames.append(attr)
+ allAttrStrings = stringAttrNames + binaryAttrNames
+ if not allAttrStrings:
+ # not AllAttrStrings means propFilter.filter_name is not mapped
+ # return None to try to match all items if this is the only property filter
+ return (None, [], [])
+
+ constant = ABDirectoryQueryResult.constantProperties.get(propFilter.filter_name)
+ if propFilter.qualifier and isinstance(propFilter.qualifier, addressbookqueryfilter.IsNotDefined):
+ return definedExpression(False, filterAllOf, propFilter.filter_name, constant, queryAttributes, allAttrStrings)
+
+ paramFilterElements = [paramFilterElement for paramFilterElement in propFilter.filters if isinstance(paramFilterElement, addressbookqueryfilter.ParameterFilter)]
+ textMatchElements = [textMatchElement for textMatchElement in propFilter.filters if isinstance(textMatchElement, addressbookqueryfilter.TextMatch)]
+
+ # if only one propFilter, then use filterAllOf as propFilterAllOf to reduce subexpressions and simplify generated query string
+ if len(propFilter.filters) == 1:
+ propFilterAllOf = filterAllOf
+ else:
+ propFilterAllOf = propFilter.propfilter_test == "allof"
+
+ # handle parameter filter elements
+ if len(paramFilterElements) > 0:
+ if supportedParamter(propFilter.filter_name, paramFilterElements, propFilterAllOf ):
+ if len(textMatchElements) == 0:
+ return definedExpression(True, filterAllOf, propFilter.filter_name, constant, queryAttributes, allAttrStrings)
+ else:
+ if propFilterAllOf:
+ return (False, [], [])
+
+ # handle text match elements
+ propFilterNeedsAllRecords = propFilterAllOf
+ propFilterAttributes = []
+ propFilterExpressionList = []
+ for textMatchElement in textMatchElements:
+
+ textMatchNeedsAllRecords, textMatchExpressionAttributes, textMatchExpression = textMatchElementExpression(propFilterAllOf, textMatchElement)
+ if propFilterAllOf:
+ propFilterNeedsAllRecords &= textMatchNeedsAllRecords
+ else:
+ propFilterNeedsAllRecords |= textMatchNeedsAllRecords
+ propFilterAttributes += textMatchExpressionAttributes
+ propFilterExpressionList += textMatchExpression
+
+
+ if (len(propFilterExpressionList) > 1) and (filterAllOf != propFilterAllOf):
+ propFilterExpressions = [dsquery.expression(dsquery.expression.AND if propFilterAllOf else dsquery.expression.OR , list(set(propFilterExpressionList)))] # remove duplicates
+ else:
+ propFilterExpressions = list(set(propFilterExpressionList))
+
+ return (propFilterNeedsAllRecords, propFilterAttributes, propFilterExpressions)
+ #end propFilterExpression
+
+ #print("propFilterListQuery: filterAllOf=%r, propFilters=%r" % (filterAllOf, propFilters,))
+ """
+ Create an expression for a list of prop-filter elements.
+
+ @param filterAllOf: the C{True} if parent filter test is "allof"
+ @param propFilters: the C{list} of L{ComponentFilter} elements.
+ @return: (needsAllRecords, espressionAttributes, expression) tuple
+ """
+ needsAllRecords = None
+ attributes = []
+ expressions = []
+ for propFilter in propFilters:
+
+ propNeedsAllRecords, propExpressionAttributes, propExpression = propFilterExpression(filterAllOf, propFilter)
+ if needsAllRecords is None:
+ needsAllRecords = propNeedsAllRecords
+ elif propNeedsAllRecords is not None:
+ if filterAllOf:
+ needsAllRecords &= propNeedsAllRecords
+ else:
+ needsAllRecords |= propNeedsAllRecords
+ attributes += propExpressionAttributes
+ expressions += propExpression
+ # propFilterExpression()'s returned propNeedsAllRecords is only None if a propFilter.filter_name is not not mapped
+ # needsAllRecords is None if there was only one propFilter that returned None
+ # set needsAllRecords True in the case
+ if needsAllRecords is None:
+ needsAllRecords = not filterAllOf
+
+ if len(expressions) > 1:
+ expr = dsquery.expression(dsquery.expression.AND if filterAllOf else dsquery.expression.OR , list(set(expressions))) # remove duplicates
+ elif len(expressions):
+ expr = expressions[0]
+ else:
+ expr = None
+
+ return (needsAllRecords, attributes, expr)
+
+
+ #print("dsFilterFromAddressBookFilter")
+ # Lets assume we have a valid filter from the outset
+
+ # Top-level filter contains zero or more prop-filters
+ if addressBookFilter:
+ filterAllOf = addressBookFilter.filter_test == "allof"
+ if len(addressBookFilter.children) > 0:
+ return propFilterListQuery(filterAllOf, addressBookFilter.children)
+ else:
+ return (filterAllOf, [], [])
+ else:
+ return (False, [], [])
+
+
+
+class ABDirectoryQueryResult(DAVPropertyMixIn, LoggingMixIn):
+ """
+ Result from ab query report or multiget on directory
+ """
+
+ # od attributes that may contribute to vcard properties
+ # will be used to translate vCard queries to od queries
+
+ vcardPropToDSAttrMap = {
+
+ "FN" : [
+ dsattributes.kDS1AttrFirstName,
+ dsattributes.kDS1AttrLastName,
+ dsattributes.kDS1AttrMiddleName,
+ dsattributes.kDSNAttrNamePrefix,
+ dsattributes.kDSNAttrNameSuffix,
+ dsattributes.kDS1AttrDistinguishedName,
+ dsattributes.kDSNAttrRecordName,
+ ],
+ "N" : [
+ dsattributes.kDS1AttrFirstName,
+ dsattributes.kDS1AttrLastName,
+ dsattributes.kDS1AttrMiddleName,
+ dsattributes.kDSNAttrNamePrefix,
+ dsattributes.kDSNAttrNameSuffix,
+ dsattributes.kDS1AttrDistinguishedName,
+ dsattributes.kDSNAttrRecordName,
+ ],
+ "NICKNAME" : [
+ dsattributes.kDSNAttrNickName,
+ ],
+ # no binary searching
+ "PHOTO" : [
+ (dsattributes.kDSNAttrJPEGPhoto, "base64"),
+ ],
+ "BDAY" : [
+ dsattributes.kDS1AttrBirthday,
+ ],
+ "ADR" : [
+ dsattributes.kDSNAttrBuilding,
+ dsattributes.kDSNAttrStreet,
+ dsattributes.kDSNAttrCity,
+ dsattributes.kDSNAttrState,
+ dsattributes.kDSNAttrPostalCode,
+ dsattributes.kDSNAttrCountry,
+ ],
+ "LABEL" : [
+ dsattributes.kDSNAttrPostalAddress,
+ dsattributes.kDSNAttrPostalAddressContacts,
+ dsattributes.kDSNAttrAddressLine1,
+ dsattributes.kDSNAttrAddressLine2,
+ dsattributes.kDSNAttrAddressLine3,
+ ],
+ "TEL" : [
+ dsattributes.kDSNAttrPhoneNumber,
+ dsattributes.kDSNAttrMobileNumber,
+ dsattributes.kDSNAttrPagerNumber,
+ dsattributes.kDSNAttrHomePhoneNumber,
+ dsattributes.kDSNAttrPhoneContacts,
+ dsattributes.kDSNAttrFaxNumber,
+ #dsattributes.kDSNAttrAreaCode,
+ ],
+ "EMAIL" : [
+ dsattributes.kDSNAttrEMailAddress,
+ dsattributes.kDSNAttrEMailContacts,
+ ],
+ "GEO" : [
+ dsattributes.kDSNAttrMapCoordinates,
+ ],
+ "TITLE" : [
+ dsattributes.kDSNAttrJobTitle,
+ ],
+ "ORG" : [
+ dsattributes.kDSNAttrCompany,
+ dsattributes.kDSNAttrOrganizationName,
+ dsattributes.kDSNAttrDepartment,
+ ],
+ "NOTE" : [
+ dsattributes.kDS1AttrComment,
+ dsattributes.kDS1AttrNote,
+ ],
+ "REV" : [
+ dsattributes.kDS1AttrModificationTimestamp,
+ ],
+ "UID" : [
+ dsattributes.kDS1AttrGeneratedUID,
+ dsattributes.kDSNAttrRecordName,
+ dsattributes.kDSNAttrRecordType,
+ ],
+ "URL" : [
+ dsattributes.kDS1AttrWeblogURI,
+ dsattributes.kDSNAttrURL,
+ ],
+ "KEY" : [
+ # check on format, are these all binary?
+ (dsattributes.kDSNAttrPGPPublicKey, "base64"),
+ (dsattributes.kDS1AttrUserCertificate, "base64"),
+ (dsattributes.kDS1AttrUserPKCS12Data, "base64"),
+ (dsattributes.kDS1AttrUserSMIMECertificate, "base64"),
+ ],
+ "IMPP" : [
+ dsattributes.kDSNAttrIMHandle,
+ ],
+ "X-ABRELATEDNAMES" : [
+ dsattributes.kDSNAttrRelationships,
+ ],
+ "X-INTERNAL-MINIMUM-VCARD-PROPERTIES" : [
+ dsattributes.kDS1AttrGeneratedUID,
+ dsattributes.kDS1AttrFirstName,
+ dsattributes.kDS1AttrLastName,
+ dsattributes.kDS1AttrMiddleName,
+ dsattributes.kDSNAttrNamePrefix,
+ dsattributes.kDSNAttrNameSuffix,
+ dsattributes.kDS1AttrDistinguishedName,
+ dsattributes.kDSNAttrRecordName,
+ dsattributes.kDSNAttrRecordType,
+ dsattributes.kDS1AttrModificationTimestamp,
+ dsattributes.kDS1AttrCreationTimestamp,
+ ],
+ "X-INTERNAL-REQUIRED" : [
+ dsattributes.kDS1AttrGeneratedUID,
+ dsattributes.kDS1AttrDistinguishedName,
+ dsattributes.kDSNAttrRecordName,
+ dsattributes.kDS1AttrFirstName,
+ dsattributes.kDS1AttrLastName,
+ dsattributes.kDSNAttrRecordType,
+ ],
+
+ }
+
+ allDSQueryAttributes = list(set([attr for lookupAttributes in vcardPropToDSAttrMap.values()
+ for attr in lookupAttributes]))
+
+ binaryDSAttrNames = [attr[0] for attr in allDSQueryAttributes
+ if isinstance(attr, tuple) ]
+
+ stringDSAttrNames = [attr for attr in allDSQueryAttributes
+ if isinstance(attr, str) ]
+
+ allDSAttrNames = stringDSAttrNames + binaryDSAttrNames
+
+ #peopleUIDSeparator = "-" + OpenDirectoryBackingService.baseGUID + "-"
+ userUIDSeparator = "-bf07a1a2-"
+ peopleUIDSeparator = "-cf07a1a2-"
+
+
+ constantProperties = {
+ # 3.6.3 PRODID Type Definition
+ "PRODID": vCardProductID,
+ # 3.6.9 VERSION Type Definition
+ "VERSION": "3.0",
+ }
+
+
+ def __init__(self, directoryBackedAddressBook, recordAttributes, generateSimpleUIDs=False, addDSAttrXProperties=False, appleInternalServer=False, ):
+
+
+ self.log_debug("directoryBackedAddressBook=%s, attributes=%s" % (directoryBackedAddressBook, recordAttributes))
+
+ #save off for debugging
+ self.addDSAttrXProperties = addDSAttrXProperties;
+ if addDSAttrXProperties:
+ self.originalAttributes = recordAttributes.copy()
+ self.appleInternalServer = appleInternalServer
+
+ self._directoryBackedAddressBook = directoryBackedAddressBook
+ self._vCard = None
+
+ #clean attributes
+ self.attributes = {}
+ for key, values in recordAttributes.items():
+ if key in ABDirectoryQueryResult.stringDSAttrNames:
+ if isinstance(values, list):
+ self.attributes[key] = [removeControlChars(val).decode("utf8") for val in values]
+ else:
+ self.attributes[key] = removeControlChars(values).decode("utf8")
+ else:
+ self.attributes[key] = values
+
+ # find a GUID
+ guid = self.firstValueForAttribute(dsattributes.kDS1AttrGeneratedUID)
+ if not guid:
+ if generateSimpleUIDs:
+ nodeUUIDStr = "00000000"
+ else:
+ nodeUUIDStr = "%x" % abs(hash(node))
+ nameUUIDStr = "".join(self.firstValueForAttribute(dsattributes.kDSNAttrRecordName).encode("base64").split("\n"))
+ if self.firstValueForAttribute(dsattributes.kDSNAttrRecordType) != dsattributes.kDSStdRecordTypePeople:
+ guid = ABDirectoryQueryResult.userUIDSeparator.join([nodeUUIDStr, nameUUIDStr,])
+ else:
+ guid = ABDirectoryQueryResult.peopleUIDSeparator.join([nodeUUIDStr, nameUUIDStr,])
+
+ self.attributes[dsattributes.kDS1AttrGeneratedUID] = guid
+
+ #generate a vCard here. May throw an exception
+ self.vCard()
+
+
+
+ def __repr__(self):
+ return "<%s[%s(%s)]>" % (
+ self.__class__.__name__,
+ self.vCard().propertyValue("FN"),
+ self.vCard().propertyValue("UID")
+ )
+
+ def __hash__(self):
+ s = "".join([
+ "%s:%s" % (attribute, self.valuesForAttribute(attribute),)
+ for attribute in self.attributes
+ ])
+ return hash(s)
+
+
+ def hasAttribute(self, attributeName ):
+ return self.valuesForAttribute(attributeName, None) is not None
+
+
+ def valuesForAttribute(self, attributeName, default_values=[] ):
+ values = self.attributes.get(attributeName)
+ if (values is None):
+ return default_values
+ elif not isinstance(values, list):
+ values = [values, ]
+
+ # ds templates often return empty attribute values
+ # get rid of them here
+ nonEmptyValues = [(value.encode("utf-8") if isinstance(value, unicode) else value) for value in values if len(value) > 0 ]
+
+ if len(nonEmptyValues) > 0:
+ return nonEmptyValues
+ else:
+ return default_values
+
+
+ def firstValueForAttribute(self, attributeName, default_value="" ):
+ values = self.attributes.get(attributeName)
+ if values is None:
+ return default_value
+ elif isinstance(values, list):
+ return values[0].encode("utf_8") if isinstance(values[0], unicode) else values[0]
+ else:
+ return values.encode("utf_8") if isinstance(values, unicode) else values
+
+ def joinedValuesForAttribute(self, attributeName, separator=",", default_string="" ):
+ values = self.valuesForAttribute(attributeName, None)
+ if not values:
+ return default_string
+ else:
+ return separator.join(values)
+
+
+ def isoDateStringForDateAttribute(self, attributeName, default_string="" ):
+ modDate = self.firstValueForAttribute(attributeName, default_string)
+ revDate = None
+ if modDate:
+ if len(modDate) >= len("YYYYMMDD") and modDate[:8].isdigit():
+ revDate = "%s-%s-%s" % (modDate[:4],modDate[4:6],modDate[6:8], )
+ if len(modDate) >= len("YYYYMMDDHHMMSS") and modDate[8:14].isdigit():
+ revDate += "T%s:%s:%sZ" % (modDate[8:10],modDate[10:12],modDate[12:14], )
+ return revDate
+
+
+
+ def vCard(self):
+
+
+ def generateVCard():
+
+ def isUniqueProperty(vcard, newProperty, ignoreParams = None):
+ existingProperties = vcard.properties(newProperty.name())
+ for existingProperty in existingProperties:
+ if ignoreParams:
+ existingProperty = existingProperty.duplicate()
+ for paramname, paramvalue in ignoreParams:
+ existingProperty.removeParameterValue(paramname, paramvalue)
+ if existingProperty == newProperty:
+ return False
+ return True
+
+ def addUniqueProperty(vcard, newProperty, ignoreParams = None, attrType = None, attrValue = None):
+ if isUniqueProperty(vcard, newProperty, ignoreParams):
+ vcard.addProperty(newProperty)
+ else:
+ if attrType and attrValue:
+ self.log_info("Ignoring attribute %r with value %r in creating property %r. A duplicate property already exists." % (attrType, attrValue, newProperty, ))
+
+ def addPropertyAndLabel(groupCount, label, propertyName, propertyValue, parameters = None ):
+ groupCount[0] += 1
+ groupPrefix = "item%d" % groupCount[0]
+ vcard.addProperty(Property(propertyName, propertyValue, params=parameters, group=groupPrefix))
+ vcard.addProperty(Property("X-ABLabel", label, group=groupPrefix))
+
+ # for attributes of the form param:value
+ def addPropertiesAndLabelsForPrefixedAttribute(groupCount, propertyPrefix, propertyName, attrType, defaultLabel, nolabelParamTypes=(), labelMap={}, specialParamType=None):
+ preferred = True
+ for attrValue in self.valuesForAttribute(attrType):
+ try:
+ # special case for Apple
+ if self.appleInternalServer and attrType == dsattributes.kDSNAttrIMHandle:
+ splitValue = attrValue.split("|")
+ if len (splitValue) > 1:
+ attrValue = splitValue[0]
+ if splitValue[1].upper() in nolabelParamTypes:
+ defaultLabel = splitValue[1]
+
+ colonIndex = attrValue.find(":")
+ if (colonIndex > len(attrValue)-2):
+ raise ValueError("Nothing after colon.")
+
+ propertyValue = attrValue[colonIndex+1:]
+ labelString = attrValue[:colonIndex] if colonIndex > 0 else defaultLabel
+ paramTypeString = labelString.upper()
+
+ if specialParamType:
+ parameters = { specialParamType: (paramTypeString,) }
+ if preferred:
+ parameters["TYPE"] = ("PREF",)
+ else:
+ # add PREF to first prop's parameters
+ paramTypeStrings = [paramTypeString,]
+ if preferred and "PREF" != paramTypeString:
+ paramTypeStrings += ["PREF",]
+ parameters = { "TYPE": paramTypeStrings, }
+
+ #special case for IMHandles which the param is the last part of the property like X-AIM or X-JABBER
+ if propertyPrefix:
+ propertyName = propertyPrefix + paramTypeString
+
+ # only add label prop if needed
+ if paramTypeString in nolabelParamTypes:
+ addUniqueProperty(vcard, Property(propertyName, attrValue[colonIndex+1:], params=parameters), None, attrValue, attrType)
+ else:
+ # use special localizable addressbook labels where possible
+ localizedABLabelString = labelMap.get(labelString, labelString)
+ addPropertyAndLabel(groupCount, localizedABLabelString, propertyName, propertyValue, parameters)
+ preferred = False
+
+ except Exception, e:
+ traceback.print_exc()
+ self.log_debug("addPropertiesAndLabelsForPrefixedAttribute(): groupCount=%r, propertyPrefix=%r, propertyName=%r, nolabelParamTypes=%r, labelMap=%r, attrType=%r" % (groupCount[0], propertyPrefix, propertyName, nolabelParamTypes, labelMap, attrType,))
+ self.log_error("addPropertiesAndLabelsForPrefixedAttribute(): Trouble parsing attribute %s, with value \"%s\". Error = %s" % (attrType, attrValue, e,))
+
+
+ #print("ABDirectoryQueryResult.vCard")
+ # create vCard
+ vcard = Component("VCARD")
+ groupCount = [0]
+
+ # add constant properties - properties that are the same regardless of the record attributes
+ for key, value in ABDirectoryQueryResult.constantProperties.items():
+ vcard.addProperty(Property(key, value))
+
+ # 3.1 IDENTIFICATION TYPES http://tools.ietf.org/html/rfc2426#section-3.1
+ # 3.1.1 FN Type Definition
+ # dsattributes.kDS1AttrDistinguishedName, # Users distinguished or real name
+ #
+ # full name is required but this is set in OpenDiretoryBackingRecord.__init__
+ #vcard.addProperty(Property("FN", self.firstValueForAttribute(dsattributes.kDS1AttrDistinguishedName)))
+
+ # 3.1.2 N Type Definition
+ # dsattributes.kDS1AttrFirstName, # Used for first name of user or person record.
+ # dsattributes.kDS1AttrLastName, # Used for the last name of user or person record.
+ # dsattributes.kDS1AttrMiddleName, #Used for the middle name of user or person record.
+ # dsattributes.kDSNAttrNameSuffix, # Represents the name suffix of a user or person.
+ # ie. Jr., Sr., etc.
+ # Usually found in user or people records (kDSStdRecordTypeUsers or
+ # dsattributes.kDSStdRecordTypePeople).
+ # dsattributes.kDSNAttrNamePrefix, # Represents the title prefix of a user or person.
+ # ie. Mr., Ms., Mrs., Dr., etc.
+ # Usually found in user or people records (kDSStdRecordTypeUsers or
+ # dsattributes.kDSStdRecordTypePeople).
+
+ # name is required, so make sure we have one
+ # vcard says: Each name attribute can be a string or a list of strings.
+ if not self.hasAttribute(dsattributes.kDS1AttrFirstName) and not self.hasAttribute(dsattributes.kDS1AttrLastName):
+ familyName = self.firstValueForAttribute(dsattributes.kDS1AttrDistinguishedName)
+ else:
+ familyName = self.valuesForAttribute(dsattributes.kDS1AttrLastName, "")
+
+ nameObject = N(
+ first = self.valuesForAttribute(dsattributes.kDS1AttrFirstName, ""),
+ last = familyName,
+ middle = self.valuesForAttribute(dsattributes.kDS1AttrMiddleName, ""),
+ prefix = self.valuesForAttribute(dsattributes.kDSNAttrNamePrefix, ""),
+ suffix = self.valuesForAttribute(dsattributes.kDSNAttrNameSuffix, ""),
+ )
+ vcard.addProperty(Property("N", nameObject))
+
+ # set full name to Name with contiguous spaces stripped
+ # it turns out that Address Book.app ignores FN and creates it fresh from N in ABRecord
+ # so no reason to have FN distinct from N
+ vcard.addProperty(Property("FN", nameObject.getFullName() ))
+
+ # 3.1.3 NICKNAME Type Definition
+ # dsattributes.kDSNAttrNickName, # Represents the nickname of a user or person.
+ # Usually found in user or people records (kDSStdRecordTypeUsers or
+ # dsattributes.kDSStdRecordTypePeople).
+ for nickname in self.valuesForAttribute(dsattributes.kDSNAttrNickName):
+ addUniqueProperty(vcard, Property("NICKNAME", nickname), None, dsattributes.kDSNAttrNickName, nickname)
+
+ # 3.1.4 PHOTO Type Definition
+ # dsattributes.kDSNAttrJPEGPhoto, # Used to store binary picture data in JPEG format.
+ # Usually found in user, people or group records (kDSStdRecordTypeUsers,
+ # dsattributes.kDSStdRecordTypePeople,dsattributes.kDSStdRecordTypeGroups).
+ # pyOpenDirectory always returns binary-encoded string
+
+ for photo in self.valuesForAttribute(dsattributes.kDSNAttrJPEGPhoto):
+ photo = "".join("".join(photo.split("\r")).split("\n")) #get rid of line folding: for PHOTO
+ addUniqueProperty(vcard, Property("PHOTO", photo, params={"ENCODING": ["b",], "TYPE": ["JPEG",],}), None, dsattributes.kDSNAttrJPEGPhoto, photo)
+
+
+ # 3.1.5 BDAY Type Definition
+ # dsattributes.kDS1AttrBirthday, # Single-valued attribute that defines the user's birthday.
+ # Format is x.208 standard YYYYMMDDHHMMSSZ which we will require as GMT time.
+ # 012345678901234
+
+ birthdate = self.isoDateStringForDateAttribute(dsattributes.kDS1AttrBirthday)
+ if birthdate:
+ vcard.addProperty(Property("BDAY", PyCalendarDateTime.parseText(birthdate, fullISO=True)))
+
+
+ # 3.2 Delivery Addressing Types http://tools.ietf.org/html/rfc2426#section-3.2
+ #
+ # 3.2.1 ADR Type Definition
+
+ #address
+ # vcard says: Each address attribute can be a string or a list of strings.
+ extended = self.valuesForAttribute(dsattributes.kDSNAttrBuilding, "")
+ street = self.valuesForAttribute(dsattributes.kDSNAttrStreet, "")
+ city = self.valuesForAttribute(dsattributes.kDSNAttrCity, "")
+ region = self.valuesForAttribute(dsattributes.kDSNAttrState, "")
+ code = self.valuesForAttribute(dsattributes.kDSNAttrPostalCode, "")
+ country = self.valuesForAttribute(dsattributes.kDSNAttrCountry, "")
+
+ if len(extended) > 0 or len(street) > 0 or len(city) > 0 or len(region) > 0 or len(code) > 0 or len(country) > 0:
+ vcard.addProperty(Property("ADR",
+ Adr(
+ #pobox = box,
+ extended = extended,
+ street = street,
+ locality = city,
+ region = region,
+ postalcode = code,
+ country = country,
+ ),
+ params = {"TYPE": ["WORK", "PREF", "POSTAL", "PARCEL",],}
+ ))
+
+
+ # 3.2.2 LABEL Type Definition
+
+ # dsattributes.kDSNAttrPostalAddress, # The postal address usually excluding postal code.
+ # dsattributes.kDSNAttrPostalAddressContacts, # multi-valued attribute that defines a record's alternate postal addresses .
+ # found in user records (kDSStdRecordTypeUsers) and resource records (kDSStdRecordTypeResources).
+ # dsattributes.kDSNAttrAddressLine1, # Line one of multiple lines of address data for a user.
+ # dsattributes.kDSNAttrAddressLine2, # Line two of multiple lines of address data for a user.
+ # dsattributes.kDSNAttrAddressLine3, # Line three of multiple lines of address data for a user.
+
+ for label in self.valuesForAttribute(dsattributes.kDSNAttrPostalAddress):
+ addUniqueProperty(vcard, Property("LABEL", label, params={"TYPE": ["POSTAL", "PARCEL",]}), None, dsattributes.kDSNAttrPostalAddress, label)
+
+ for label in self.valuesForAttribute(dsattributes.kDSNAttrPostalAddressContacts):
+ addUniqueProperty(vcard, Property("LABEL", label, params={"TYPE": ["POSTAL", "PARCEL",]}), None, dsattributes.kDSNAttrPostalAddressContacts, label)
+
+ address = self.joinedValuesForAttribute(dsattributes.kDSNAttrAddressLine1)
+ addressLine2 = self.joinedValuesForAttribute(dsattributes.kDSNAttrAddressLine2)
+ if len(addressLine2) > 0:
+ address += "\n" + addressLine2
+ addressLine3 = self.joinedValuesForAttribute(dsattributes.kDSNAttrAddressLine3)
+ if len(addressLine3) > 0:
+ address += "\n" + addressLine3
+
+ if len(address) > 0:
+ vcard.addProperty(Property("LABEL", address, params={"TYPE": ["POSTAL", "PARCEL",]}))
+
+ # 3.3 TELECOMMUNICATIONS ADDRESSING TYPES http://tools.ietf.org/html/rfc2426#section-3.3
+ # 3.3.1 TEL Type Definition
+ # TEL;TYPE=work,voice,pref,msg:+1-213-555-1234
+
+ # dsattributes.kDSNAttrPhoneNumber, # Telephone number of a user.
+ # dsattributes.kDSNAttrMobileNumber, # Represents the mobile numbers of a user or person.
+ # Usually found in user or people records (kDSStdRecordTypeUsers or
+ # dsattributes.kDSStdRecordTypePeople).
+ # dsattributes.kDSNAttrFaxNumber, # Represents the FAX numbers of a user or person.
+ # Usually found in user or people records (kDSStdRecordTypeUsers or
+ # kDSStdRecordTypePeople).
+ # dsattributes.kDSNAttrPagerNumber, # Represents the pager numbers of a user or person.
+ # Usually found in user or people records (kDSStdRecordTypeUsers or
+ # dsattributes.kDSStdRecordTypePeople).
+ # dsattributes.kDSNAttrHomePhoneNumber, # Home telephone number of a user or person.
+ # dsattributes.kDSNAttrPhoneContacts, # multi-valued attribute that defines a record's custom phone numbers .
+ # found in user records (kDSStdRecordTypeUsers).
+ # Example: home fax:408-555-4444
+
+ params = {"TYPE": ["WORK", "PREF", "VOICE",],}
+ for phone in self.valuesForAttribute(dsattributes.kDSNAttrPhoneNumber):
+ addUniqueProperty(vcard, Property("TEL", phone, params=params), (("TYPE", "PREF"),), phone, dsattributes.kDSNAttrPhoneNumber)
+ params = {"TYPE": ["WORK", "VOICE",],}
+
+ params = { "TYPE": ["WORK", "PREF", "CELL",], }
+ for phone in self.valuesForAttribute(dsattributes.kDSNAttrMobileNumber):
+ addUniqueProperty(vcard, Property("TEL", phone, params=params), (("TYPE", "PREF"),), phone, dsattributes.kDSNAttrMobileNumber)
+ params = { "TYPE": ["WORK", "CELL",], }
+
+ params = { "TYPE": ["WORK", "PREF", "FAX",], }
+ for phone in self.valuesForAttribute(dsattributes.kDSNAttrFaxNumber):
+ addUniqueProperty(vcard, Property("TEL", phone, params=params), (("TYPE", "PREF"),), phone, dsattributes.kDSNAttrFaxNumber)
+ params = { "TYPE": ["WORK", "FAX",], }
+
+ params = { "TYPE": ["WORK", "PREF", "PAGER",], }
+ for phone in self.valuesForAttribute(dsattributes.kDSNAttrPagerNumber):
+ addUniqueProperty(vcard, Property("TEL", phone, params=params), (("TYPE", "PREF"),), phone, dsattributes.kDSNAttrPagerNumber)
+ params = { "TYPE": ["WORK", "PAGER",], }
+
+ params = { "TYPE": ["HOME", "PREF", "VOICE",], }
+ for phone in self.valuesForAttribute(dsattributes.kDSNAttrHomePhoneNumber):
+ addUniqueProperty(vcard, Property("TEL", phone, params=params), (("TYPE", "PREF"),), phone, dsattributes.kDSNAttrHomePhoneNumber)
+ params = { "TYPE": ["HOME", "VOICE",], }
+
+ addPropertiesAndLabelsForPrefixedAttribute(groupCount=groupCount, propertyPrefix=None, propertyName="TEL", defaultLabel="work",
+ nolabelParamTypes=("VOICE", "CELL", "FAX", "PAGER",),
+ attrType=dsattributes.kDSNAttrPhoneContacts, )
+
+ """
+ # EXTEND: Use this attribute
+ # dsattributes.kDSNAttrAreaCode, # Area code of a user's phone number.
+ """
+
+ # 3.3.2 EMAIL Type Definition
+ # dsattributes.kDSNAttrEMailAddress, # Email address of usually a user record.
+
+ # setup some params
+ preferredWorkParams = { "TYPE": ["WORK", "PREF", "INTERNET",], }
+ workParams = { "TYPE": ["WORK", "INTERNET",], }
+ params = preferredWorkParams
+ for emailAddress in self.valuesForAttribute(dsattributes.kDSNAttrEMailAddress):
+ addUniqueProperty(vcard, Property("EMAIL", emailAddress, params=params), (("TYPE", "PREF"),), emailAddress, dsattributes.kDSNAttrEMailAddress)
+ params = workParams
+
+ # dsattributes.kDSNAttrEMailContacts, # multi-valued attribute that defines a record's custom email addresses .
+ # found in user records (kDSStdRecordTypeUsers).
+ # Example: home:johndoe at mymail.com
+
+ # check to see if parameters type are open ended. Could be any string
+ addPropertiesAndLabelsForPrefixedAttribute(groupCount=groupCount, propertyPrefix=None, propertyName="EMAIL", defaultLabel="work",
+ nolabelParamTypes=("WORK", "HOME",),
+ attrType=dsattributes.kDSNAttrEMailContacts, )
+
+ """
+ # UNIMPLEMENTED:
+ # 3.3.3 MAILER Type Definition
+ """
+ # 3.4 GEOGRAPHICAL TYPES http://tools.ietf.org/html/rfc2426#section-3.4
+ """
+ # UNIMPLEMENTED:
+ # 3.4.1 TZ Type Definition
+ """
+ # 3.4.2 GEO Type Definition
+ #dsattributes.kDSNAttrMapCoordinates, # attribute that defines coordinates for a user's location .
+ # Found in user records (kDSStdRecordTypeUsers) and resource records (kDSStdRecordTypeResources).
+ # Example: 7.7,10.6
+ for coordinate in self.valuesForAttribute(dsattributes.kDSNAttrMapCoordinates):
+ parts = coordinate.split(",")
+ if (len(parts) == 2):
+ vcard.addProperty(Property("GEO", parts))
+ else:
+ self.log_info("Ignoring malformed attribute %r with value %r. Well-formed example: 7.7,10.6." % (dsattributes.kDSNAttrMapCoordinates, coordinate))
+ #
+ # 3.5 ORGANIZATIONAL TYPES http://tools.ietf.org/html/rfc2426#section-3.5
+ #
+ # 3.5.1 TITLE Type Definition
+ for jobTitle in self.valuesForAttribute(dsattributes.kDSNAttrJobTitle):
+ addUniqueProperty(vcard, Property("TITLE", jobTitle), None, dsattributes.kDSNAttrJobTitle, jobTitle)
+
+ """
+ # UNIMPLEMENTED:
+ # 3.5.2 ROLE Type Definition
+ # 3.5.3 LOGO Type Definition
+ # 3.5.4 AGENT Type Definition
+ """
+ # 3.5.5 ORG Type Definition
+ company = self.joinedValuesForAttribute(dsattributes.kDSNAttrCompany)
+ if len(company) == 0:
+ company = self.joinedValuesForAttribute(dsattributes.kDSNAttrOrganizationName)
+ department = self.joinedValuesForAttribute(dsattributes.kDSNAttrDepartment)
+ extra = self.joinedValuesForAttribute(dsattributes.kDSNAttrOrganizationInfo)
+ if len(company) > 0 or len(department) > 0:
+ vcard.addProperty(Property("ORG", (company, department, extra, ),))
+
+ # 3.6 EXPLANATORY TYPES http://tools.ietf.org/html/rfc2426#section-3.6
+ """
+ # UNIMPLEMENTED:
+ # 3.6.1 CATEGORIES Type Definition
+ """
+ # 3.6.2 NOTE Type Definition
+ # dsattributes.kDS1AttrComment, # Attribute used for unformatted comment.
+ # dsattributes.kDS1AttrNote, # Note attribute. Commonly used in printer records.
+ for comment in self.valuesForAttribute(dsattributes.kDS1AttrComment):
+ addUniqueProperty(vcard, Property("NOTE", comment), None, dsattributes.kDS1AttrComment, comment)
+
+ for note in self.valuesForAttribute(dsattributes.kDS1AttrNote):
+ addUniqueProperty(vcard, Property("NOTE", note), None, dsattributes.kDS1AttrNote, note)
+
+ # 3.6.3 PRODID Type Definition
+ #vcard.addProperty(Property("PRODID", vCardProductID + "//BUILD %s" % twistedcaldav.__version__))
+ #vcard.addProperty(Property("PRODID", vCardProductID))
+ # ADDED WITH CONTSTANT PROPERTIES
+
+ # 3.6.4 REV Type Definition
+ revDate = self.isoDateStringForDateAttribute(dsattributes.kDS1AttrModificationTimestamp)
+ if revDate:
+ vcard.addProperty(Property("REV", PyCalendarDateTime.parseText(revDate, fullISO=True)))
+
+ """
+ # UNIMPLEMENTED:
+ # 3.6.5 SORT-STRING Type Definition
+ # 3.6.6 SOUND Type Definition
+ """
+ # 3.6.7 UID Type Definition
+ # dsattributes.kDS1AttrGeneratedUID, # Used for 36 character (128 bit) unique ID. Usually found in user,
+ # group, and computer records. An example value is "A579E95E-CDFE-4EBC-B7E7-F2158562170F".
+ # The standard format contains 32 hex characters and four hyphen characters.
+
+ vcard.addProperty(Property("UID", self.firstValueForAttribute(dsattributes.kDS1AttrGeneratedUID)))
+
+
+ # 3.6.8 URL Type Definition
+ # dsattributes.kDSNAttrURL, # List of URLs.
+ # dsattributes.kDS1AttrWeblogURI, # Single-valued attribute that defines the URI of a user's weblog.
+ # Usually found in user records (kDSStdRecordTypeUsers).
+ # Example: http://example.com/blog/jsmith
+ for url in self.valuesForAttribute(dsattributes.kDS1AttrWeblogURI):
+ addPropertyAndLabel(groupCount, "weblog", "URL", url, parameters = {"TYPE": ["Weblog",]})
+
+ for url in self.valuesForAttribute(dsattributes.kDSNAttrURL):
+ addPropertyAndLabel(groupCount, "_$!<HomePage>!$_", "URL", url, parameters = {"TYPE": ["Homepage",]})
+
+
+ # 3.6.9 VERSION Type Definition
+ # ALREADY ADDED
+
+ # 3.7 SECURITY TYPES http://tools.ietf.org/html/rfc2426#section-3.7
+ # 3.7.1 CLASS Type Definition
+ # ALREADY ADDED
+
+ # 3.7.2 KEY Type Definition
+
+ # dsattributes.kDSNAttrPGPPublicKey, # Pretty Good Privacy public encryption key.
+ # dsattributes.kDS1AttrUserCertificate, # Attribute containing the binary of the user's certificate.
+ # Usually found in user records. The certificate is data which identifies a user.
+ # This data is attested to by a known party, and can be independently verified
+ # by a third party.
+ # dsattributes.kDS1AttrUserPKCS12Data, # Attribute containing binary data in PKCS #12 format.
+ # Usually found in user records. The value can contain keys, certificates,
+ # and other related information and is encrypted with a passphrase.
+ # dsattributes.kDS1AttrUserSMIMECertificate,# Attribute containing the binary of the user's SMIME certificate.
+ # Usually found in user records. The certificate is data which identifies a user.
+ # This data is attested to by a known party, and can be independently verified
+ # by a third party. SMIME certificates are often used for signed or encrypted
+ # emails.
+
+ for key in self.valuesForAttribute(dsattributes.kDSNAttrPGPPublicKey):
+ addUniqueProperty(vcard, Property("KEY", key, params = {"ENCODING": ["b",], "TYPE": ["PGPPublicKey",]}), None, dsattributes.kDSNAttrPGPPublicKey, key)
+
+ for key in self.valuesForAttribute(dsattributes.kDS1AttrUserCertificate):
+ addUniqueProperty(vcard, Property("KEY", key, params = {"ENCODING": ["b",], "TYPE": ["UserCertificate",]}), None, dsattributes.kDS1AttrUserCertificate, key)
+
+ for key in self.valuesForAttribute(dsattributes.kDS1AttrUserPKCS12Data):
+ addUniqueProperty(vcard, Property("KEY", key, params = {"ENCODING": ["b",], "TYPE": ["UserPKCS12Data",]}), None, dsattributes.kDS1AttrUserPKCS12Data, key)
+
+ for key in self.valuesForAttribute(dsattributes.kDS1AttrUserSMIMECertificate):
+ addUniqueProperty(vcard, Property("KEY", key, params = {"ENCODING": ["b",], "TYPE": ["UserSMIMECertificate",]}), None, dsattributes.kDS1AttrUserSMIMECertificate, key)
+
+ """
+ X- attributes, Address Book support
+ """
+ # X-AIM, X-JABBER, X-MSN, X-YAHOO, X-ICQ
+ # instant messaging
+ # dsattributes.kDSNAttrIMHandle, # Represents the Instant Messaging handles of a user.
+ # Values should be prefixed with the appropriate IM type
+ # ie. AIM:, Jabber:, MSN:, Yahoo:, or ICQ:
+ # Usually found in user records (kDSStdRecordTypeUsers).
+ imNolabelParamTypes=("AIM", "FACEBOOK", "GAGU-GAGU", "GOOGLE TALK", "ICQ", "JABBER", "MSN", "QQ", "SKYPE", "YAHOO",)
+ addPropertiesAndLabelsForPrefixedAttribute(groupCount=groupCount, propertyPrefix="X-", propertyName=None, defaultLabel="aim",
+ nolabelParamTypes=imNolabelParamTypes,
+ attrType=dsattributes.kDSNAttrIMHandle,)
+
+
+
+ # IMPP
+ # Address Book's implementation of http://tools.ietf.org/html/rfc6350#section-6.4.3
+ # adding IMPP property allows ab query report search on one property
+ addPropertiesAndLabelsForPrefixedAttribute(groupCount=groupCount, propertyPrefix=None, propertyName="IMPP", defaultLabel="aim",
+ specialParamType = "X-SERVICE-TYPE",
+ nolabelParamTypes=imNolabelParamTypes,
+ attrType=dsattributes.kDSNAttrIMHandle,)
+
+ # X-ABRELATEDNAMES
+ # dsattributes.kDSNAttrRelationships, # multi-valued attribute that defines the relationship to the record type .
+ # found in user records (kDSStdRecordTypeUsers).
+ # Example: brother:John
+ addPropertiesAndLabelsForPrefixedAttribute(groupCount=groupCount, propertyPrefix=None, propertyName="X-ABRELATEDNAMES", defaultLabel="friend",
+ labelMap={ "FATHER":"_$!<Father>!$_",
+ "MOTHER":"_$!<Mother>!$_",
+ "PARENT":"_$!<Parent>!$_",
+ "BROTHER":"_$!<Brother>!$_",
+ "SISTER":"_$!<Sister>!$_",
+ "CHILD":"_$!<Child>!$_",
+ "FRIEND":"_$!<Friend>!$_",
+ "SPOUSE":"_$!<Spouse>!$_",
+ "PARTNER":"_$!<Partner>!$_",
+ "ASSISTANT":"_$!<Assistant>!$_",
+ "MANAGER":"_$!<Manager>!$_", },
+ attrType=dsattributes.kDSNAttrRelationships, )
+
+
+ # special case for Apple
+ if self.appleInternalServer:
+ for manager in self.valuesForAttribute("dsAttrTypeNative:appleManager"):
+ splitManager = manager.split("|")
+ if len(splitManager) >= 4:
+ managerValue = "%s %s, %s" % (splitManager[0], splitManager[1], splitManager[3],)
+ elif len(splitManager) >= 2:
+ managerValue = "%s %s" % (splitManager[0], splitManager[1])
+ else:
+ managerValue = manager
+ addPropertyAndLabel( groupCount, "_$!<Manager>!$_", "X-ABRELATEDNAMES", managerValue, parameters={ "TYPE": ["Manager",]} )
+
+ """
+ # UNIMPLEMENTED: X- attributes
+
+ X-MAIDENNAME
+ X-PHONETIC-FIRST-NAME
+ X-PHONETIC-MIDDLE-NAME
+ X-PHONETIC-LAST-NAME
+
+ sattributes.kDS1AttrPicture, # Represents the path of the picture for each user displayed in the login window.
+ # Found in user records (kDSStdRecordTypeUsers).
+
+ dsattributes.kDS1AttrMapGUID, # Represents the GUID for a record's map.
+ dsattributes.kDSNAttrMapURI, # attribute that defines the URI of a user's location.
+
+ dsattributes.kDSNAttrOrganizationInfo, # Usually the organization info of a user.
+ dsattributes.kDSNAttrAreaCode, # Area code of a user's phone number.
+
+ dsattributes.kDSNAttrMIME, # Data contained in this attribute type is a fully qualified MIME Type.
+
+ """
+
+ # debug, create x attributes for all ds attributes
+ if self.addDSAttrXProperties:
+ for attribute in self.originalAttributes:
+ for value in self.valuesForAttribute(attribute):
+ vcard.addProperty(Property("X-"+"-".join(attribute.split(":")), removeControlChars(value)))
+
+ return vcard
+
+
+ if not self._vCard:
+ self._vCard = generateVCard()
+
+ return self._vCard
+
+ def vCardText(self):
+ return str(self.vCard())
+
+ def uriName(self):
+ return self.vCard().getProperty("UID").value() + ".vcf"
+
+ def hRef(self, parentURI="/directory/"):
+ # FIXME: Get the parent URI from self._directoryBackedAddressBook
+ return davxml.HRef.fromString(joinURL(parentURI, self.uriName()))
+
+
+ def readProperty(self, property, request):
+
+ if type(property) is tuple:
+ qname = property
+ else:
+ qname = property.qname()
+
+ namespace, name = qname
+
+ #print("ABDirectoryQueryResult.readProperty: qname = %s" % (qname, ))
+
+ if namespace == dav_namespace:
+ if name == "resourcetype":
+ result = davxml.ResourceType.empty #@UndefinedVariable
+ #print("ABDirectoryQueryResult.readProperty: qname = %s, result = %s" % (qname, result))
+ return result
+ elif name == "getetag":
+ result = davxml.GETETag( ETag(hashlib.md5(self.vCardText()).hexdigest()).generate() )
+ #print("ABDirectoryQueryResult.readProperty: qname = %s, result = %s" % (qname, result))
+ return result
+ elif name == "getcontenttype":
+ mimeType = MimeType('text', 'vcard', {})
+ result = davxml.GETContentType(generateContentType(mimeType))
+ #print("ABDirectoryQueryResult.readProperty: qname = %s, result = %s" % (qname, result))
+ return result
+ elif name == "getcontentlength":
+ result = davxml.GETContentLength.fromString(str(len(self.vCardText())))
+ #print("ABDirectoryQueryResult.readProperty: qname = %s, result = %s" % (qname, result))
+ return result
+ elif name == "getlastmodified":
+ if self.vCard().hasProperty("REV"):
+ modDatetime = parse_date(self.vCard().propertyValue("REV"))
+ else:
+ # use creation date attribute if it exists
+ creationDateString = self.isoDateStringForDateAttribute(dsattributes.kDS1AttrCreationTimestamp)
+ if creationDateString:
+ modDatetime = parse_date(creationDateString)
+ else:
+ modDatetime = datetime.datetime.utcnow()
+
+ #strip time zone because time zones are unimplemented in davxml.GETLastModified.fromDate
+ d = modDatetime.date()
+ t = modDatetime.time()
+ modDatetimeNoTZ = datetime.datetime(d.year, d.month, d.day, t.hour, t.minute, t.second, t.microsecond, None)
+ result = davxml.GETLastModified.fromDate(modDatetimeNoTZ)
+ #print("ABDirectoryQueryResult.readProperty: qname = %s, result = %s" % (qname, result))
+ return result
+ elif name == "creationdate":
+ creationDateString = self.isoDateStringForDateAttribute(dsattributes.kDS1AttrCreationTimestamp)
+ if creationDateString:
+ creationDatetime = parse_date(creationDateString)
+ elif self.vCard().hasProperty("REV"): # use modification date property if it exists
+ creationDatetime = parse_date(self.vCard().propertyValue("REV"))
+ else:
+ creationDatetime = datetime.datetime.utcnow()
+ result = davxml.CreationDate.fromDate(creationDatetime)
+ #print("ABDirectoryQueryResult.readProperty: qname = %s, result = %s" % (qname, result))
+ return result
+ elif name == "displayname":
+ # AddressBook.app uses N. Use FN or UID instead?
+ result = davxml.DisplayName.fromString(self.vCard().propertyValue("N"))
+ #print("ABDirectoryQueryResult.readProperty: qname = %s, result = %s" % (qname, result))
+ return result
+
+ elif namespace == twisted_dav_namespace:
+ return super(ABDirectoryQueryResult, self).readProperty(property, request)
+ #return DAVPropertyMixIn.readProperty(self, property, request)
+
+ return self._directoryBackedAddressBook.readProperty(property, request)
+
+ def listProperties(self, request):
+ #print("ABDirectoryQueryResult.listProperties()")
+ qnames = set(self.liveProperties())
+
+ # Add dynamic live properties that exist
+ dynamicLiveProperties = (
+ (dav_namespace, "quota-available-bytes" ),
+ (dav_namespace, "quota-used-bytes" ),
+ )
+ for dqname in dynamicLiveProperties:
+ #print("ABDirectoryQueryResult.listProperties: removing dqname=%s" % (dqname,))
+ qnames.remove(dqname)
+
+ for qname in self.deadProperties().list():
+ if (qname not in qnames) and (qname[0] != twisted_private_namespace):
+ #print("listProperties: adding qname=%s" % (qname,))
+ qnames.add(qname)
+
+ #for qn in qnames: print("ABDirectoryQueryResult.listProperties: qn=%s" % (qn,))
+
+ yield qnames
+
+ listProperties = deferredGenerator(listProperties)
+
+# utility
+#remove illegal XML
+def removeControlChars( utf8String ):
+ result = ''.join([c for c in utf8String if c not in "\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"])
+ return result
+
+
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20120406/2a625616/attachment-0001.html>
More information about the calendarserver-changes
mailing list