<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[12279] twext/trunk</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.calendarserver.org//changeset/12279">12279</a></dd>
<dt>Author</dt> <dd>wsanchez@apple.com</dd>
<dt>Date</dt> <dd>2014-01-09 19:39:29 -0800 (Thu, 09 Jan 2014)</dd>
</dl>

<h3>Log Message</h3>
<pre>More LDAP</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#twexttrunkpyflakes">twext/trunk/pyflakes</a></li>
<li><a href="#twexttrunksetuppy">twext/trunk/setup.py</a></li>
<li><a href="#twexttrunktwextwhoopendirectory__init__py">twext/trunk/twext/who/opendirectory/__init__.py</a></li>
<li><a href="#twexttrunktwextwhoopendirectory_constantspy">twext/trunk/twext/who/opendirectory/_constants.py</a></li>
<li><a href="#twexttrunktwextwhoopendirectory_servicepy">twext/trunk/twext/who/opendirectory/_service.py</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li>twext/trunk/twext/who/ldap/</li>
<li><a href="#twexttrunktwextwholdap__init__py">twext/trunk/twext/who/ldap/__init__.py</a></li>
<li><a href="#twexttrunktwextwholdap_constantspy">twext/trunk/twext/who/ldap/_constants.py</a></li>
<li><a href="#twexttrunktwextwholdap_servicepy">twext/trunk/twext/who/ldap/_service.py</a></li>
<li><a href="#twexttrunktwextwholdap_utilpy">twext/trunk/twext/who/ldap/_util.py</a></li>
<li>twext/trunk/twext/who/ldap/test/</li>
<li><a href="#twexttrunktwextwholdaptest__init__py">twext/trunk/twext/who/ldap/test/__init__.py</a></li>
<li><a href="#twexttrunktwextwholdaptesttest_servicepy">twext/trunk/twext/who/ldap/test/test_service.py</a></li>
<li><a href="#twexttrunktwextwholdaptesttest_utilpy">twext/trunk/twext/who/ldap/test/test_util.py</a></li>
</ul>

<h3>Removed Paths</h3>
<ul>
<li><a href="#twexttrunktwextwholdappy">twext/trunk/twext/who/ldap.py</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="twexttrunkpyflakes"></a>
<div class="modfile"><h4>Modified: twext/trunk/pyflakes (12278 => 12279)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/pyflakes        2014-01-09 22:18:11 UTC (rev 12278)
+++ twext/trunk/pyflakes        2014-01-10 03:39:29 UTC (rev 12279)
</span><span class="lines">@@ -23,5 +23,5 @@
</span><span class="cx"> 
</span><span class="cx"> . &quot;${wd}/develop&quot;;
</span><span class="cx"> 
</span><del>-pip install pyflakes --target=&quot;${dev_libdir}&quot; &gt; &quot;${dev_root}/pip_pyflakes.log&quot;;
</del><ins>+pip install pyflakes --no-use-wheel --upgrade --target=&quot;${dev_libdir}&quot; &gt; &quot;${dev_root}/pip_pyflakes.log&quot; || true;
</ins><span class="cx"> exec &quot;${python}&quot; -m pyflakes &quot;$@&quot;;
</span></span></pre></div>
<a id="twexttrunksetuppy"></a>
<div class="modfile"><h4>Modified: twext/trunk/setup.py (12278 => 12279)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/setup.py        2014-01-09 22:18:11 UTC (rev 12278)
+++ twext/trunk/setup.py        2014-01-10 03:39:29 UTC (rev 12279)
</span><span class="lines">@@ -116,8 +116,9 @@
</span><span class="cx"> ]
</span><span class="cx"> 
</span><span class="cx"> install_requirements = [
</span><ins>+    &quot;twisted&gt;=13.2.0&quot;,
</ins><span class="cx">     &quot;sqlparse==0.1.2&quot;,
</span><del>-    &quot;twisted&gt;=13.2.0&quot;,
</del><ins>+    &quot;python-ldap&gt;=2.4.13&quot;,
</ins><span class="cx"> ]
</span><span class="cx"> 
</span><span class="cx"> 
</span></span></pre></div>
<a id="twexttrunktwextwholdap__init__py"></a>
<div class="addfile"><h4>Added: twext/trunk/twext/who/ldap/__init__.py (0 => 12279)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/twext/who/ldap/__init__.py                                (rev 0)
+++ twext/trunk/twext/who/ldap/__init__.py        2014-01-10 03:39:29 UTC (rev 12279)
</span><span class="lines">@@ -0,0 +1,34 @@
</span><ins>+##
+# Copyright (c) 2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+# 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 &quot;AS IS&quot; 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.
+##
+
+__all__ = [
+    &quot;LDAPError&quot;,
+    &quot;LDAPConnectionError&quot;,
+    # &quot;LDAPQueryError&quot;,
+    # &quot;LDAPDataError&quot;,
+    &quot;DirectoryService&quot;,
+]
+
+
+try:
+    from ._service import (
+        LDAPError, LDAPConnectionError,  # LDAPQueryError, LDAPDataError,
+        DirectoryService,
+    )
+except ImportError:
+    raise
+    LDAPError = LDAPConnectionError = None
+    # LDAPQueryError = LDAPDataError = None
</ins></span></pre></div>
<a id="twexttrunktwextwholdap_constantspy"></a>
<div class="addfile"><h4>Added: twext/trunk/twext/who/ldap/_constants.py (0 => 12279)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/twext/who/ldap/_constants.py                                (rev 0)
+++ twext/trunk/twext/who/ldap/_constants.py        2014-01-10 03:39:29 UTC (rev 12279)
</span><span class="lines">@@ -0,0 +1,99 @@
</span><ins>+##
+# Copyright (c) 2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+# 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 &quot;AS IS&quot; 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.
+##
+
+&quot;&quot;&quot;
+LDAP constants.
+&quot;&quot;&quot;
+
+from twisted.python.constants import (
+    Names, NamedConstant,  # Values, ValueConstant
+)
+
+from ..expression import MatchType
+
+
+
+class LDAPMatchType(Names):
+    &quot;&quot;&quot;
+    Constants for native LDAP match types.
+
+    @cvar any: Attribute has any value.
+    @cvar equals: Attribute equals value.
+    @cvar startsWith: Attribute starts with value.
+    @cvar endsWith: Attribute ends with value.
+    @cvar contains: Attribute contains value.
+    @cvar lessThan: Attribute is less than value.
+    @cvar greaterThan: Attribute is greater than value.
+    @cvar lessThanOrEqualTo: Attribute is less than or equal to value.
+    @cvar greaterThanOrEqualTo: Attribute is greater than or equal to value.
+
+    &quot;&quot;&quot;
+
+    any = NamedConstant()
+    any.queryString = u&quot;({notOp}{attribute}=*)&quot;
+
+    equals = NamedConstant()
+    equals.matchType = MatchType.equals
+    equals.queryString = u&quot;({notOp}{attribute}={value})&quot;
+
+    startsWith = NamedConstant()
+    startsWith.matchType = MatchType.startsWith
+    startsWith.queryString = u&quot;({notOp}{attribute}={value}*)&quot;
+
+    endsWith = NamedConstant()
+    endsWith.matchType = MatchType.endsWith
+    endsWith.queryString = u&quot;({notOp}{attribute}=*{value})&quot;
+
+    contains = NamedConstant()
+    contains.matchType = MatchType.contains
+    contains.queryString = u&quot;({notOp}{attribute}=*{value}*)&quot;
+
+    lessThan = NamedConstant()
+    lessThan.matchType = MatchType.lessThan
+    lessThan.queryString = u&quot;({notOp}{attribute}&lt;{value})&quot;
+
+    greaterThan = NamedConstant()
+    greaterThan.matchType = MatchType.greaterThan
+    greaterThan.queryString = u&quot;({notOp}{attribute}&gt;{value})&quot;
+
+    lessThanOrEqualTo = NamedConstant()
+    lessThanOrEqualTo.matchType = MatchType.lessThanOrEqualTo
+    lessThanOrEqualTo.queryString = u&quot;({notOp}{attribute}&lt;={value})&quot;
+
+    greaterThanOrEqualTo = NamedConstant()
+    greaterThanOrEqualTo.matchType = MatchType.greaterThanOrEqualTo
+    greaterThanOrEqualTo.queryString = u&quot;({notOp}{attribute}&gt;={value})&quot;
+
+
+    @classmethod
+    def fromMatchType(cls, matchType):
+        &quot;&quot;&quot;
+        Look up an L{LDAPMatchType} from a L{MatchType}.
+
+        @param matchType: A match type.
+        @type matchType: L{MatchType}
+
+        @return: The cooresponding LDAP match type.
+        @rtype: L{LDAPMatchType}
+        &quot;&quot;&quot;
+        if not hasattr(cls, &quot;_matchTypeByMatchType&quot;):
+            cls._matchTypeByMatchType = dict((
+                (matchType.matchType, matchType)
+                for matchType in cls.iterconstants()
+                if hasattr(matchType, &quot;matchType&quot;)
+            ))
+
+        return cls._matchTypeByMatchType.get(matchType, None)
</ins></span></pre></div>
<a id="twexttrunktwextwholdap_servicepyfromrev12278twexttrunktwextwholdappy"></a>
<div class="copfile"><h4>Copied: twext/trunk/twext/who/ldap/_service.py (from rev 12278, twext/trunk/twext/who/ldap.py) (0 => 12279)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/twext/who/ldap/_service.py                                (rev 0)
+++ twext/trunk/twext/who/ldap/_service.py        2014-01-10 03:39:29 UTC (rev 12279)
</span><span class="lines">@@ -0,0 +1,201 @@
</span><ins>+# -*- test-case-name: twext.who.ldap.test.test_service -*-
+##
+# Copyright (c) 2013-2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+# 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 &quot;AS IS&quot; BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from __future__ import print_function
+
+&quot;&quot;&quot;
+LDAP directory service implementation.
+&quot;&quot;&quot;
+
+import ldap
+
+# from zope.interface import implementer
+
+# from twisted.internet.defer import succeed, fail
+from twisted.cred.credentials import IUsernamePassword
+# from twisted.web.guard import DigestCredentialFactory
+
+from twext.python.log import Logger
+
+from ..idirectory import (
+    DirectoryServiceError, DirectoryAvailabilityError,
+    # InvalidDirectoryRecordError, QueryNotSupportedError,
+    # FieldName as BaseFieldName,
+    RecordType as BaseRecordType,
+    # IPlaintextPasswordVerifier, IHTTPDigestVerifier,
+)
+from ..directory import (
+    DirectoryService as BaseDirectoryService,
+    DirectoryRecord as BaseDirectoryRecord,
+)
+# from ..expression import (
+#     CompoundExpression, Operand,
+#     MatchExpression, MatchFlags,
+# )
+from ..util import (
+    # iterFlags,
+    ConstantsContainer,
+)
+# from ._constants import LDAP_QUOTING_TABLE
+
+
+
+#
+# Exceptions
+#
+
+class LDAPError(DirectoryServiceError):
+    &quot;&quot;&quot;
+    LDAP error.
+    &quot;&quot;&quot;
+
+    def __init__(self, message, ldapError=None):
+        super(LDAPError, self).__init__(message)
+        self.ldapError = ldapError
+
+
+
+class LDAPConnectionError(DirectoryAvailabilityError):
+    &quot;&quot;&quot;
+    LDAP connection error.
+    &quot;&quot;&quot;
+
+    def __init__(self, message, ldapError=None):
+        super(LDAPConnectionError, self).__init__(message)
+        self.ldapError = ldapError
+
+
+
+# class LDAPQueryError(LDAPError):
+#     &quot;&quot;&quot;
+#     LDAP query error.
+#     &quot;&quot;&quot;
+
+
+
+# class LDAPDataError(LDAPError):
+#     &quot;&quot;&quot;
+#     LDAP data error.
+#     &quot;&quot;&quot;
+
+
+
+#
+# Directory Service
+#
+
+class DirectoryService(BaseDirectoryService):
+    &quot;&quot;&quot;
+    LDAP directory service.
+    &quot;&quot;&quot;
+
+    log = Logger()
+
+    recordType = ConstantsContainer((
+        BaseRecordType.user, BaseRecordType.group,
+    ))
+
+
+    def __init__(
+        self,
+        url=&quot;ldap://localhost/&quot;,
+        credentials=None,
+        tlsCACertificateFile=None,
+        tlsCACertificateDirectory=None,
+        useTLS=False,
+    ):
+        self._url = url
+        self.credentials = credentials
+        self._tlsCACertificateFile = tlsCACertificateFile
+        self._tlsCACertificateDirectory = tlsCACertificateDirectory
+        self._useTLS = useTLS,
+
+
+    @property
+    def realmName(self):
+        return u&quot;{self.url}&quot;.format(self=self)
+
+
+    @property
+    def connection(self):
+        &quot;&quot;&quot;
+        Get the underlying LDAP connection.
+        &quot;&quot;&quot;
+        self._connect()
+        return self._connection
+
+
+    def _connect(self):
+        &quot;&quot;&quot;
+        Connect to the directory server.
+
+        @raises: L{LDAPConnectionError} if unable to connect.
+        &quot;&quot;&quot;
+        if not hasattr(self, &quot;_connection&quot;):
+            self.log.info(&quot;Connecting to LDAP at {source.url}&quot;)
+            connection = ldap.initialize(self._url)
+            # FIXME: Use trace_file option to wire up debug logging when
+            # Twisted adopts the new logging stuff.
+
+            def valueFor(constant):
+                if constant is None:
+                    return None
+                else:
+                    return constant.value
+
+            for option, value in (
+                (ldap.OPT_X_TLS_CACERTFILE, self._tlsCACertificateFile),
+                (ldap.OPT_X_TLS_CACERTDIR, self._tlsCACertificateDirectory),
+            ):
+                if value is not None:
+                    connection.set_option(option, value)
+
+            if self._useTLS:
+                connection.start_tls_s()
+
+            if self.credentials is not None:
+                if IUsernamePassword.providedBy(self.credentials):
+                    try:
+                        self.ldap.simple_bind_s(
+                            self.credentials.username,
+                            self.credentials.password,
+                        )
+                        self.log.info(
+                            &quot;Bound to LDAP as {credentials.username}&quot;,
+                            credentials=self.credentials
+                        )
+                    except ldap.INVALID_CREDENTIALS as e:
+                        self.log.info(
+                            &quot;Unable to bind to LDAP as {credentials.username}&quot;,
+                            credentials=self.credentials
+                        )
+                        raise LDAPConnectionError(str(e), e)
+
+                else:
+                    raise LDAPConnectionError(
+                        &quot;Unknown credentials type: {0}&quot;
+                        .format(self.credentials)
+                    )
+
+            self._connection = connection
+
+
+
+class DirectoryRecord(BaseDirectoryRecord):
+    &quot;&quot;&quot;
+    LDAP directory record.
+    &quot;&quot;&quot;
</ins></span></pre></div>
<a id="twexttrunktwextwholdap_utilpy"></a>
<div class="addfile"><h4>Added: twext/trunk/twext/who/ldap/_util.py (0 => 12279)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/twext/who/ldap/_util.py                                (rev 0)
+++ twext/trunk/twext/who/ldap/_util.py        2014-01-10 03:39:29 UTC (rev 12279)
</span><span class="lines">@@ -0,0 +1,165 @@
</span><ins>+# -*- test-case-name: twext.who.ldap.test.test_util -*-
+##
+# Copyright (c) 2013-2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+# 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 &quot;AS IS&quot; BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from ..idirectory import QueryNotSupportedError
+from ..expression import (
+    CompoundExpression, Operand,
+    MatchExpression, MatchFlags,
+)
+from ..util import iterFlags
+from ._constants import LDAPMatchType
+
+
+
+def ldapQueryStringFromMatchExpression(expression, attrMap):
+    &quot;&quot;&quot;
+    Generates an LDAP query string from a match expression.
+
+    @param expression: A match expression.
+    @type expression: L{MatchExpression}
+
+    @param attrMap: A mapping from L{FieldName}s to native LDAP attribute
+        names.
+    @type attrMap: L{dict}
+
+    @return: An LDAP query string.
+    @rtype: C{unicode}
+
+    @raises QueryNotSupportedError: If the expression's match type is unknown,
+        or if the expresion references an unknown field name (meaning a field
+        name not in C{attrMap}).
+    &quot;&quot;&quot;
+    matchType = LDAPMatchType.fromMatchType(expression.matchType)
+    if matchType is None:
+        raise QueryNotSupportedError(
+            &quot;Unknown match type: {0}&quot;.format(matchType)
+        )
+
+    flags = tuple(iterFlags(expression.flags))
+
+    if MatchFlags.NOT in flags:
+        notOp = u&quot;!&quot;
+    else:
+        notOp = u&quot;&quot;
+
+    # FIXME: It doesn't look like LDAP queries can be case sensitive.
+    # This would mean that it's up to the callers to filter out the false
+    # positives...
+    #
+    # if MatchFlags.caseInsensitive not in flags:
+    #     raise NotImplementedError(&quot;Need to handle case sensitive&quot;)
+
+    try:
+        attribute = attrMap[expression.fieldName]
+    except KeyError:
+        raise QueryNotSupportedError(
+            &quot;Unknown field name: {0}&quot;.format(expression.fieldName)
+        )
+
+    value = unicode(expression.fieldValue)       # We want unicode
+    value = value.translate(LDAP_QUOTING_TABLE)  # Escape special chars
+
+    return matchType.queryString.format(
+        notOp=notOp, attribute=attribute, value=value
+    )
+
+
+def ldapQueryStringFromCompoundExpression(expression, attrMap):
+    &quot;&quot;&quot;
+    Generates an LDAP query string from a compound expression.
+
+    @param expression: A compound expression.
+    @type expression: L{MatchExpression}
+
+    @param attrMap: A mapping from L{FieldName}s to native LDAP attribute
+        names.
+    @type attrMap: L{dict}
+
+    @return: An LDAP query string.
+    @rtype: C{unicode}
+
+    @raises QueryNotSupportedError: If any sub-expression cannot be converted
+        to an LDAP query.
+    &quot;&quot;&quot;
+    queryTokens = []
+
+    if len(expression.expressions) &gt; 1:
+        queryTokens.append(u&quot;(&quot;)
+
+        if expression.operand is Operand.AND:
+            queryTokens.append(u&quot;&amp;&quot;)
+        else:
+            queryTokens.append(u&quot;|&quot;)
+
+    for subExpression in expression.expressions:
+        queryTokens.append(
+            ldapQueryStringFromExpression(subExpression, attrMap)
+        )
+
+    if len(expression.expressions) &gt; 1:
+        queryTokens.append(u&quot;)&quot;)
+
+    return u&quot;&quot;.join(queryTokens)
+
+
+def ldapQueryStringFromExpression(expression, attrMap):
+    &quot;&quot;&quot;
+    Converts an expression into an LDAP query string.
+
+    @param attrMap: A mapping from L{FieldName}s to native LDAP attribute
+        names.
+    @type attrMap: L{dict}
+
+    @param expression: An expression.
+    @type expression: L{MatchExpression} or L{CompoundExpression}
+
+    @return: A native OpenDirectory query string
+    @rtype: C{unicode}
+
+    @raises QueryNotSupportedError: If the expression cannot be converted to an
+        LDAP query.
+    &quot;&quot;&quot;
+
+    if isinstance(expression, MatchExpression):
+        return ldapQueryStringFromMatchExpression(expression, attrMap)
+
+    if isinstance(expression, CompoundExpression):
+        return ldapQueryStringFromCompoundExpression(expression, attrMap)
+
+    raise QueryNotSupportedError(
+        &quot;Unknown expression type: {0!r}&quot;.format(expression)
+    )
+
+
+LDAP_QUOTING_TABLE = {
+    ord(u&quot;\\&quot;): u&quot;\\5C&quot;,
+    ord(u&quot;/&quot;): u&quot;\\2F&quot;,
+
+    ord(u&quot;(&quot;): u&quot;\\28&quot;,
+    ord(u&quot;)&quot;): u&quot;\\29&quot;,
+    ord(u&quot;*&quot;): u&quot;\\2A&quot;,
+
+    ord(u&quot;&lt;&quot;): u&quot;\\3C&quot;,
+    ord(u&quot;=&quot;): u&quot;\\3D&quot;,
+    ord(u&quot;&gt;&quot;): u&quot;\\3E&quot;,
+    ord(u&quot;~&quot;): u&quot;\\7E&quot;,
+
+    ord(u&quot;&amp;&quot;): u&quot;\\26&quot;,
+    ord(u&quot;|&quot;): u&quot;\\7C&quot;,
+
+    ord(u&quot;\0&quot;): u&quot;\\00&quot;,
+}
</ins></span></pre></div>
<a id="twexttrunktwextwholdaptest__init__py"></a>
<div class="addfile"><h4>Added: twext/trunk/twext/who/ldap/test/__init__.py (0 => 12279)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/twext/who/ldap/test/__init__.py                                (rev 0)
+++ twext/trunk/twext/who/ldap/test/__init__.py        2014-01-10 03:39:29 UTC (rev 12279)
</span><span class="lines">@@ -0,0 +1,15 @@
</span><ins>+##
+# Copyright (c) 2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+# 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 &quot;AS IS&quot; 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.
+##
</ins></span></pre></div>
<a id="twexttrunktwextwholdaptesttest_servicepy"></a>
<div class="addfile"><h4>Added: twext/trunk/twext/who/ldap/test/test_service.py (0 => 12279)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/twext/who/ldap/test/test_service.py                                (rev 0)
+++ twext/trunk/twext/who/ldap/test/test_service.py        2014-01-10 03:39:29 UTC (rev 12279)
</span><span class="lines">@@ -0,0 +1,33 @@
</span><ins>+##
+# Copyright (c) 2013-2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+# 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 &quot;AS IS&quot; 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.
+##
+
+&quot;&quot;&quot;
+LDAP directory service tests.
+&quot;&quot;&quot;
+
+from twisted.trial import unittest
+
+# from ...expression import (
+#     CompoundExpression, Operand, MatchExpression, MatchType, MatchFlags
+# )
+# from ..ldap import DirectoryService
+
+
+
+class LDAPServiceTestCase(unittest.TestCase):
+    &quot;&quot;&quot;
+    Tests for L{DirectoryService}.
+    &quot;&quot;&quot;
</ins></span></pre></div>
<a id="twexttrunktwextwholdaptesttest_utilpy"></a>
<div class="addfile"><h4>Added: twext/trunk/twext/who/ldap/test/test_util.py (0 => 12279)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/twext/who/ldap/test/test_util.py                                (rev 0)
+++ twext/trunk/twext/who/ldap/test/test_util.py        2014-01-10 03:39:29 UTC (rev 12279)
</span><span class="lines">@@ -0,0 +1,219 @@
</span><ins>+##
+# Copyright (c) 2010-2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+# 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 &quot;AS IS&quot; 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.
+##
+
+&quot;&quot;&quot;
+OpenDirectory service tests.
+&quot;&quot;&quot;
+
+from twisted.trial import unittest
+
+from ...expression import (
+    CompoundExpression, Operand, MatchExpression, MatchType, MatchFlags
+)
+from .._service import DirectoryService
+from .._util import (
+    ldapQueryStringFromMatchExpression,
+)
+
+
+
+class LDAPQueryTestCase(unittest.TestCase):
+    &quot;&quot;&quot;
+    Tests for LDAP query generation.
+    &quot;&quot;&quot;
+
+    def attrMap(self, service):
+        &quot;&quot;&quot;
+        Create a mapping from field names to LDAP attribute names.
+        The attribute names returned here are not real LDAP attribute names,
+        but we don't care for these tests, since we're not actually connecting
+        to LDAP.
+        &quot;&quot;&quot;
+        return dict([(c, c.name) for c in service.fieldName.iterconstants()])
+
+
+    def test_queryStringFromMatchExpression_matchTypes(self):
+        &quot;&quot;&quot;
+        Match expressions with each match type produces the correct
+        operator=value string.
+        &quot;&quot;&quot;
+        service = DirectoryService()
+
+        for matchType, expected in (
+            (MatchType.equals, u&quot;=xyzzy&quot;),
+            (MatchType.startsWith, u&quot;=xyzzy*&quot;),
+            (MatchType.endsWith, u&quot;=*xyzzy&quot;),
+            (MatchType.contains, u&quot;=*xyzzy*&quot;),
+            (MatchType.lessThan, u&quot;&lt;xyzzy&quot;),
+            (MatchType.greaterThan, u&quot;&gt;xyzzy&quot;),
+            (MatchType.lessThanOrEqualTo, u&quot;&lt;=xyzzy&quot;),
+            (MatchType.greaterThanOrEqualTo, u&quot;&gt;=xyzzy&quot;),
+        ):
+            expression = MatchExpression(
+                service.fieldName.shortNames, u&quot;xyzzy&quot;,
+                matchType=matchType
+            )
+            queryString = ldapQueryStringFromMatchExpression(
+                expression, self.attrMap(service)
+            )
+            self.assertEquals(
+                queryString,
+                u&quot;({attribute}{expected})&quot;.format(
+                    attribute=&quot;shortNames&quot;, expected=expected
+                )
+            )
+
+
+    def test_queryStringFromMatchExpression_match_not(self):
+        &quot;&quot;&quot;
+        Match expression with the C{NOT} flag adds the C{!} operator.
+        &quot;&quot;&quot;
+        service = DirectoryService()
+
+        expression = MatchExpression(
+            service.fieldName.shortNames, u&quot;xyzzy&quot;,
+            flags=MatchFlags.NOT
+        )
+        queryString = ldapQueryStringFromMatchExpression(
+            expression, self.attrMap(service)
+        )
+        self.assertEquals(
+            queryString,
+            u&quot;(!{attribute}=xyzzy)&quot;.format(
+                attribute=&quot;shortNames&quot;,
+            )
+        )
+
+
+    def test_queryStringFromMatchExpression_match_caseInsensitive(self):
+        &quot;&quot;&quot;
+        Match expression with the C{caseInsensitive} flag adds the C{??????}
+        operator.
+        &quot;&quot;&quot;
+        service = DirectoryService()
+
+        expression = MatchExpression(
+            service.fieldName.shortNames, u&quot;xyzzy&quot;,
+            flags=MatchFlags.caseInsensitive
+        )
+        queryString = ldapQueryStringFromMatchExpression(
+            expression, self.attrMap(service)
+        )
+        self.assertEquals(
+            queryString,
+            u&quot;???????({attribute}=xyzzy)&quot;.format(
+                attribute=&quot;shortNames&quot;,
+            )
+        )
+
+    test_queryStringFromMatchExpression_match_caseInsensitive.todo = (
+        &quot;unimplemented&quot;
+    )
+
+
+    def test_queryStringFromMatchExpression_match_quoting(self):
+        &quot;&quot;&quot;
+        Special characters are quoted properly.
+        &quot;&quot;&quot;
+        service = DirectoryService()
+
+        expression = MatchExpression(
+            service.fieldName.fullNames,
+            u&quot;\\xyzzy: a/b/(c)* ~~ &gt;=&lt; ~~ &amp;| \0!!&quot;
+        )
+        queryString = ldapQueryStringFromMatchExpression(
+            expression, self.attrMap(service)
+        )
+        self.assertEquals(
+            queryString,
+            u&quot;({attribute}={expected})&quot;.format(
+                attribute=&quot;fullNames&quot;,
+                expected=(
+                    u&quot;\\5Cxyzzy: a\\2Fb\\2F\\28c\\29\\2A &quot;
+                    &quot;\\7E\\7E \\3E\\3D\\3C \\7E\\7E \\26\\7C \\00!!&quot;
+                )
+            )
+        )
+
+
+    # def test_queryStringFromExpression(self):
+    #     service = DirectoryService()
+
+    #     # CompoundExpressions
+
+    #     expression = CompoundExpression(
+    #         [
+    #             MatchExpression(
+    #                 service.fieldName.uid, u&quot;a&quot;,
+    #                 matchType=MatchType.contains
+    #             ),
+    #             MatchExpression(
+    #                 service.fieldName.guid, u&quot;b&quot;,
+    #                 matchType=MatchType.contains
+    #             ),
+    #             MatchExpression(
+    #                 service.fieldName.shortNames, u&quot;c&quot;,
+    #                 matchType=MatchType.contains
+    #             ),
+    #             MatchExpression(
+    #                 service.fieldName.emailAddresses, u&quot;d&quot;,
+    #                 matchType=MatchType.startsWith
+    #             ),
+    #             MatchExpression(
+    #                 service.fieldName.fullNames, u&quot;e&quot;,
+    #                 matchType=MatchType.equals
+    #             ),
+    #         ],
+    #         Operand.AND
+    #     )
+    #     queryString = service._queryStringFromExpression(expression)
+    #     self.assertEquals(
+    #         queryString,
+    #         (
+    #             u&quot;(&amp;(dsAttrTypeStandard:GeneratedUID=*a*)&quot;
+    #             u&quot;(dsAttrTypeStandard:GeneratedUID=*b*)&quot;
+    #             u&quot;(dsAttrTypeStandard:RecordName=*c*)&quot;
+    #             u&quot;(dsAttrTypeStandard:EMailAddress=d*)&quot;
+    #             u&quot;(dsAttrTypeStandard:RealName=e))&quot;
+    #         )
+    #     )
+
+    #     expression = CompoundExpression(
+    #         [
+    #             MatchExpression(
+    #                 service.fieldName.shortNames, u&quot;a&quot;,
+    #                 matchType=MatchType.contains
+    #             ),
+    #             MatchExpression(
+    #                 service.fieldName.emailAddresses, u&quot;b&quot;,
+    #                 matchType=MatchType.startsWith
+    #             ),
+    #             MatchExpression(
+    #                 service.fieldName.fullNames, u&quot;c&quot;,
+    #                 matchType=MatchType.equals
+    #             ),
+    #         ],
+    #         Operand.OR
+    #     )
+    #     queryString = service._queryStringFromExpression(expression)
+    #     self.assertEquals(
+    #         queryString,
+    #         (
+    #             u&quot;(|(dsAttrTypeStandard:RecordName=*a*)&quot;
+    #             u&quot;(dsAttrTypeStandard:EMailAddress=b*)&quot;
+    #             u&quot;(dsAttrTypeStandard:RealName=c))&quot;
+    #         )
+    #     )
</ins></span></pre></div>
<a id="twexttrunktwextwholdappy"></a>
<div class="delfile"><h4>Deleted: twext/trunk/twext/who/ldap.py (12278 => 12279)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/twext/who/ldap.py        2014-01-09 22:18:11 UTC (rev 12278)
+++ twext/trunk/twext/who/ldap.py        2014-01-10 03:39:29 UTC (rev 12279)
</span><span class="lines">@@ -1,198 +0,0 @@
</span><del>-# -*- test-case-name: twext.who.test.test_ldap -*-
-##
-# Copyright (c) 2013-2014 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
-# 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 &quot;AS IS&quot; BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from __future__ import print_function
-
-&quot;&quot;&quot;
-LDAP directory service implementation.
-&quot;&quot;&quot;
-
-import ldap
-
-# from zope.interface import implementer
-
-from twisted.python.constants import Values, ValueConstant
-# from twisted.internet.defer import succeed, fail
-# from twisted.web.guard import DigestCredentialFactory
-
-from twext.python.log import Logger
-
-from ..idirectory import (
-    # DirectoryServiceError, DirectoryAvailabilityError,
-    # InvalidDirectoryRecordError, QueryNotSupportedError,
-    # FieldName as BaseFieldName,
-    RecordType as BaseRecordType,
-    # IPlaintextPasswordVerifier, IHTTPDigestVerifier,
-)
-from ..directory import (
-    DirectoryService as BaseDirectoryService,
-    # DirectoryRecord as BaseDirectoryRecord,
-)
-# from ..expression import (
-#     CompoundExpression, Operand,
-#     MatchExpression, MatchFlags,
-# )
-from ..util import (
-    # iterFlags,
-    ConstantsContainer,
-)
-
-
-
-LDAP_QUOTING_TABLE = {
-    ord(u&quot;\\&quot;): u&quot;\\5C&quot;,
-    ord(u&quot;/&quot;): u&quot;\\2F&quot;,
-
-    ord(u&quot;(&quot;): u&quot;\\28&quot;,
-    ord(u&quot;)&quot;): u&quot;\\29&quot;,
-    ord(u&quot;*&quot;): u&quot;\\2A&quot;,
-
-    ord(u&quot;&lt;&quot;): u&quot;\\3C&quot;,
-    ord(u&quot;=&quot;): u&quot;\\3D&quot;,
-    ord(u&quot;&gt;&quot;): u&quot;\\3E&quot;,
-    ord(u&quot;~&quot;): u&quot;\\7E&quot;,
-
-    ord(u&quot;&amp;&quot;): u&quot;\\26&quot;,
-    ord(u&quot;|&quot;): u&quot;\\7C&quot;,
-
-    ord(u&quot;\0&quot;): u&quot;\\00&quot;,
-}
-
-
-
-#
-# Exceptions
-#
-
-# class LDAPError(DirectoryServiceError):
-#     &quot;&quot;&quot;
-#     LDAP error.
-#     &quot;&quot;&quot;
-
-#     def __init__(self, message, odError=None):
-#         super(LDAPError, self).__init__(message)
-#         self.odError = odError
-
-
-
-# class LDAPConnectionError(DirectoryAvailabilityError):
-#     &quot;&quot;&quot;
-#     LDAP connection error.
-#     &quot;&quot;&quot;
-
-#     def __init__(self, message, odError=None):
-#         super(LDAPConnectionError, self).__init__(message)
-#         self.odError = odError
-
-
-
-# class LDAPQueryError(LDAPError):
-#     &quot;&quot;&quot;
-#     LDAP query error.
-#     &quot;&quot;&quot;
-
-
-# class LDAPDataError(LDAPError):
-#     &quot;&quot;&quot;
-#     LDAP data error.
-#     &quot;&quot;&quot;
-
-
-
-#
-# LDAP Constants
-#
-
-class TLSRequireCertificate(Values):
-    never   = ValueConstant(ldap.OPT_X_TLS_NEVER)
-    allow   = ValueConstant(ldap.OPT_X_TLS_ALLOW)
-    attempt = ValueConstant(ldap.OPT_X_TLS_TRY)
-    demand  = ValueConstant(ldap.OPT_X_TLS_DEMAND)
-    hard    = ValueConstant(ldap.OPT_X_TLS_HARD)
-
-
-#
-# Directory Service
-#
-
-class DirectoryService(BaseDirectoryService):
-    &quot;&quot;&quot;
-    LDAP directory service.
-    &quot;&quot;&quot;
-    log = Logger()
-
-    recordType = ConstantsContainer((
-        BaseRecordType.user, BaseRecordType.group,
-    ))
-
-
-    def __init__(
-        self,
-        url=&quot;ldap://localhost/&quot;,
-        tlsCACertificateFile=None,
-        tlsCACertificateDirectory=None,
-        tlsRequireCertificate=None,
-        useTLS=False,
-    ):
-        self._url = url
-        self._tlsCACertificateFile = tlsCACertificateFile
-        self._tlsCACertificateDirectory = tlsCACertificateDirectory
-        self._tlsRequireCertificate = tlsRequireCertificate
-        self._useTLS = useTLS,
-
-
-    @property
-    def realmName(self):
-        return u&quot;{self.url}&quot;.format(self=self)
-
-
-    @property
-    def connection(self):
-        &quot;&quot;&quot;
-        Get the underlying LDAP connection.
-        &quot;&quot;&quot;
-        self._connect()
-        return self._connection
-
-
-    def _connect(self):
-        &quot;&quot;&quot;
-        Connect to the directory server.
-
-        @raises: L{LDAPConnectionError} if unable to connect.
-        &quot;&quot;&quot;
-        if not hasattr(self, &quot;_connection&quot;):
-            connection = ldap.initialize(self._url)
-
-            def valueFor(constant):
-                if constant is None:
-                    return None
-                else:
-                    return constant.value
-
-            for option, value in (
-                (ldap.OPT_X_TLS_CACERTFILE, self._tlsCACertificateFile),
-                (ldap.OPT_X_TLS_CACERTDIR, self._tlsCACertificateDirectory),
-                (ldap.OPT_X_TLS, valueFor(self._tlsRequireCertificate)),
-            ):
-                if value is not None:
-                    connection.set_option(option, value)
-
-            if self._useTLS:
-                connection.start_tls_s()
-
-            self._connection = connection
</del></span></pre></div>
<a id="twexttrunktwextwhoopendirectory__init__py"></a>
<div class="modfile"><h4>Modified: twext/trunk/twext/who/opendirectory/__init__.py (12278 => 12279)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/twext/who/opendirectory/__init__.py        2014-01-09 22:18:11 UTC (rev 12278)
+++ twext/trunk/twext/who/opendirectory/__init__.py        2014-01-10 03:39:29 UTC (rev 12279)
</span><span class="lines">@@ -24,7 +24,6 @@
</span><span class="cx">     &quot;OpenDirectoryQueryError&quot;,
</span><span class="cx">     &quot;OpenDirectoryDataError&quot;,
</span><span class="cx">     &quot;DirectoryService&quot;,
</span><del>-    &quot;DirectoryRecord&quot;,
</del><span class="cx">     &quot;NoQOPDigestCredentialFactory&quot;,
</span><span class="cx"> ]
</span><span class="cx"> 
</span></span></pre></div>
<a id="twexttrunktwextwhoopendirectory_constantspy"></a>
<div class="modfile"><h4>Modified: twext/trunk/twext/who/opendirectory/_constants.py (12278 => 12279)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/twext/who/opendirectory/_constants.py        2014-01-09 22:18:11 UTC (rev 12278)
+++ twext/trunk/twext/who/opendirectory/_constants.py        2014-01-10 03:39:29 UTC (rev 12279)
</span><span class="lines">@@ -1,4 +1,3 @@
</span><del>-# -*- test-case-name: twext.who.opendirectory.test.test_service -*-
</del><span class="cx"> ##
</span><span class="cx"> # Copyright (c) 2013-2014 Apple Inc. All rights reserved.
</span><span class="cx"> #
</span><span class="lines">@@ -388,8 +387,8 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> class ODMatchType(Values):
</span><del>-    all = ValueConstant(0x0001)
-    all.queryString = u&quot;({notOp}{attribute}=*)&quot;
</del><ins>+    any = ValueConstant(0x0001)
+    any.queryString = u&quot;({notOp}{attribute}=*)&quot;
</ins><span class="cx"> 
</span><span class="cx">     equals = ValueConstant(0x2001)
</span><span class="cx">     equals.matchType = MatchType.equals
</span></span></pre></div>
<a id="twexttrunktwextwhoopendirectory_servicepy"></a>
<div class="modfile"><h4>Modified: twext/trunk/twext/who/opendirectory/_service.py (12278 => 12279)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/twext/who/opendirectory/_service.py        2014-01-09 22:18:11 UTC (rev 12278)
+++ twext/trunk/twext/who/opendirectory/_service.py        2014-01-10 03:39:29 UTC (rev 12279)
</span><span class="lines">@@ -43,6 +43,7 @@
</span><span class="cx">     CompoundExpression, Operand,
</span><span class="cx">     MatchExpression, MatchFlags,
</span><span class="cx"> )
</span><ins>+from ..ldap._util import LDAP_QUOTING_TABLE
</ins><span class="cx"> from ..util import iterFlags, ConstantsContainer
</span><span class="cx"> 
</span><span class="cx"> from ._odframework import ODSession, ODNode, ODQuery
</span><span class="lines">@@ -52,27 +53,6 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-LDAP_QUOTING_TABLE = {
-    ord(u&quot;\\&quot;): u&quot;\\5C&quot;,
-    ord(u&quot;/&quot;): u&quot;\\2F&quot;,
-
-    ord(u&quot;(&quot;): u&quot;\\28&quot;,
-    ord(u&quot;)&quot;): u&quot;\\29&quot;,
-    ord(u&quot;*&quot;): u&quot;\\2A&quot;,
-
-    ord(u&quot;&lt;&quot;): u&quot;\\3C&quot;,
-    ord(u&quot;=&quot;): u&quot;\\3D&quot;,
-    ord(u&quot;&gt;&quot;): u&quot;\\3E&quot;,
-    ord(u&quot;~&quot;): u&quot;\\7E&quot;,
-
-    ord(u&quot;&amp;&quot;): u&quot;\\26&quot;,
-    ord(u&quot;|&quot;): u&quot;\\7C&quot;,
-
-    ord(u&quot;\0&quot;): u&quot;\\00&quot;,
-}
-
-
-
</del><span class="cx"> #
</span><span class="cx"> # Exceptions
</span><span class="cx"> #
</span><span class="lines">@@ -298,7 +278,7 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Generates an LDAP query string from a compound expression.
</span><span class="cx"> 
</span><del>-        @param expression: A match expression.
</del><ins>+        @param expression: A compound expression.
</ins><span class="cx">         @type expression: L{MatchExpression}
</span><span class="cx"> 
</span><span class="cx">         @return: An LDAP query string.
</span><span class="lines">@@ -425,7 +405,7 @@
</span><span class="cx">             recordTypes = ODRecordType.fromRecordType(
</span><span class="cx">                 expression.fieldValue
</span><span class="cx">             ).value
</span><del>-            matchType = ODMatchType.all.value
</del><ins>+            matchType = ODMatchType.any.value
</ins><span class="cx">             queryAttribute = None
</span><span class="cx">             queryValue = None
</span><span class="cx"> 
</span></span></pre>
</div>
</div>

</body>
</html>