Sfoglia il codice sorgente

ZF-7234
- Promoting Zend_Ldap improvements to trunk

git-svn-id: http://framework.zend.com/svn/framework/standard/trunk@16613 44c647ce-9c0f-0410-b52a-842ac1e357ba

ralph 16 anni fa
parent
commit
18425b66e1
80 ha cambiato i file con 18051 aggiunte e 483 eliminazioni
  1. 2332 0
      documentation/manual/en/module_specs/Zend_Ldap-API.xml
  2. 265 0
      documentation/manual/en/module_specs/Zend_Ldap-Introduction.xml
  3. 119 0
      documentation/manual/en/module_specs/Zend_Ldap-LDIF.xml
  4. 53 0
      documentation/manual/en/module_specs/Zend_Ldap-Node.xml
  5. 50 0
      documentation/manual/en/module_specs/Zend_Ldap-Server.xml
  6. 42 0
      documentation/manual/en/module_specs/Zend_Ldap-Tools.xml
  7. 142 0
      documentation/manual/en/module_specs/Zend_Ldap-Usage.xml
  8. 930 247
      library/Zend/Ldap.php
  9. 364 0
      library/Zend/Ldap/Attribute.php
  10. 197 0
      library/Zend/Ldap/Collection.php
  11. 253 0
      library/Zend/Ldap/Collection/Iterator/Default.php
  12. 46 0
      library/Zend/Ldap/Collection/Iterator/Interface.php
  13. 71 0
      library/Zend/Ldap/Converter.php
  14. 794 0
      library/Zend/Ldap/Dn.php
  15. 103 142
      library/Zend/Ldap/Exception.php
  16. 265 0
      library/Zend/Ldap/Filter.php
  17. 157 0
      library/Zend/Ldap/Filter/Abstract.php
  18. 48 0
      library/Zend/Ldap/Filter/And.php
  19. 37 0
      library/Zend/Ldap/Filter/Exception.php
  20. 107 0
      library/Zend/Ldap/Filter/Logical.php
  21. 66 0
      library/Zend/Ldap/Filter/Mask.php
  22. 75 0
      library/Zend/Ldap/Filter/Not.php
  23. 48 0
      library/Zend/Ldap/Filter/Or.php
  24. 65 0
      library/Zend/Ldap/Filter/String.php
  25. 301 0
      library/Zend/Ldap/Ldif/Encoder.php
  26. 1098 0
      library/Zend/Ldap/Node.php
  27. 485 0
      library/Zend/Ldap/Node/Abstract.php
  28. 209 0
      library/Zend/Ldap/Node/ChildrenIterator.php
  29. 67 0
      library/Zend/Ldap/Node/Collection.php
  30. 158 0
      library/Zend/Ldap/Node/RootDse.php
  31. 247 0
      library/Zend/Ldap/Node/RootDse/ActiveDirectory.php
  32. 102 0
      library/Zend/Ldap/Node/RootDse/OpenLdap.php
  33. 160 0
      library/Zend/Ldap/Node/RootDse/eDirectory.php
  34. 120 0
      library/Zend/Ldap/Node/Schema.php
  35. 103 0
      library/Zend/Ldap/Node/Schema/ActiveDirectory.php
  36. 104 0
      library/Zend/Ldap/Node/Schema/AttributeType/ActiveDirectory.php
  37. 75 0
      library/Zend/Ldap/Node/Schema/AttributeType/Interface.php
  38. 129 0
      library/Zend/Ldap/Node/Schema/AttributeType/OpenLdap.php
  39. 163 0
      library/Zend/Ldap/Node/Schema/Item.php
  40. 115 0
      library/Zend/Ldap/Node/Schema/ObjectClass/ActiveDirectory.php
  41. 83 0
      library/Zend/Ldap/Node/Schema/ObjectClass/Interface.php
  42. 175 0
      library/Zend/Ldap/Node/Schema/ObjectClass/OpenLdap.php
  43. 502 0
      library/Zend/Ldap/Node/Schema/OpenLdap.php
  44. 50 7
      tests/Zend/Ldap/AllTests.php
  45. 458 0
      tests/Zend/Ldap/AttributeTest.php
  46. 64 17
      tests/Zend/Ldap/BindTest.php
  47. 271 30
      tests/Zend/Ldap/CanonTest.php
  48. 18 0
      tests/Zend/Ldap/ConnectTest.php
  49. 65 0
      tests/Zend/Ldap/ConverterTest.php
  50. 361 0
      tests/Zend/Ldap/CopyRenameTest.php
  51. 356 0
      tests/Zend/Ldap/CrudTest.php
  52. 87 0
      tests/Zend/Ldap/Dn/AllTests.php
  53. 212 0
      tests/Zend/Ldap/Dn/CreationTest.php
  54. 60 0
      tests/Zend/Ldap/Dn/EscapingTest.php
  55. 265 0
      tests/Zend/Ldap/Dn/ExplodingTest.php
  56. 139 0
      tests/Zend/Ldap/Dn/ImplodingTest.php
  57. 89 0
      tests/Zend/Ldap/Dn/MiscTest.php
  58. 296 0
      tests/Zend/Ldap/Dn/ModificationTest.php
  59. 216 0
      tests/Zend/Ldap/FilterTest.php
  60. 86 0
      tests/Zend/Ldap/Ldif/AllTests.php
  61. 388 0
      tests/Zend/Ldap/Ldif/SimpleDecoderTest.php
  62. 267 0
      tests/Zend/Ldap/Ldif/SimpleEncoderTest.php
  63. 115 0
      tests/Zend/Ldap/Node/AllTests.php
  64. 62 0
      tests/Zend/Ldap/Node/AttributeIterationTest.php
  65. 123 0
      tests/Zend/Ldap/Node/ChildrenIterationTest.php
  66. 205 0
      tests/Zend/Ldap/Node/ChildrenTest.php
  67. 596 0
      tests/Zend/Ldap/Node/OfflineTest.php
  68. 287 0
      tests/Zend/Ldap/Node/OnlineTest.php
  69. 175 0
      tests/Zend/Ldap/Node/RootDseTest.php
  70. 312 0
      tests/Zend/Ldap/Node/SchemaTest.php
  71. 217 0
      tests/Zend/Ldap/Node/UpdateTest.php
  72. 74 40
      tests/Zend/Ldap/OfflineTest.php
  73. 142 0
      tests/Zend/Ldap/OnlineTestCase.php
  74. 184 0
      tests/Zend/Ldap/OriginalBindTest.php
  75. 123 0
      tests/Zend/Ldap/OriginalCanonTest.php
  76. 178 0
      tests/Zend/Ldap/OriginalConnectTest.php
  77. 117 0
      tests/Zend/Ldap/OriginalOfflineTest.php
  78. 303 0
      tests/Zend/Ldap/SearchTest.php
  79. 64 0
      tests/Zend/Ldap/TestCase.php
  80. 1 0
      tests/Zend/Ldap/_files/AttributeTest.input.txt

+ 2332 - 0
documentation/manual/en/module_specs/Zend_Ldap-API.xml

@@ -0,0 +1,2332 @@
+<sect1 id="zend.ldap.api">
+	<title>API overview</title>
+	<sect2 id="zend.ldap.api.configuration">
+		<title>Configuration / options</title>
+		<para>
+            The <code>Zend_Ldap</code> component accepts an array of options either supplied to the constructor or
+            through the <code>setOptions()</code> method. The permitted options are as follows:</para>
+		<table id="zend.ldap.api.configuration.table">
+			<title>Zend_Ldap Options</title>
+			<tgroup cols="2">
+				<thead>
+					<row>
+						<entry>Name</entry>
+						<entry>Description</entry>
+					</row>
+				</thead>
+				<tbody>
+					<row>
+						<entry>host</entry>
+						<entry>
+                        The default hostname of LDAP server if not supplied to <code>connect()</code> (also may be
+                        used when trying to canonicalize usernames in <code>bind()</code>).
+                    </entry>
+					</row>
+					<row>
+						<entry>port</entry>
+						<entry>
+                        Default port of LDAP server if not supplied to <code>connect()</code>.
+                    </entry>
+					</row>
+					<row>
+						<entry>useStartTls</entry>
+						<entry>
+                        Whether or not the LDAP client should use TLS (aka SSLv2) encrypted transport. A value of
+                        <code>true</code> is strongly favored in production environments to prevent passwords from
+                        be transmitted in clear text. The default value is <code>false</code>, as servers
+                        frequently require that a certificate be installed separately after installation.
+                        The <code>useSsl</code> and <code>useStartTls</code> options are mutually exclusive.
+                        The <code>useStartTls</code> option should be favored over <code>useSsl</code> but
+                        not all servers support this newer mechanism.
+                    </entry>
+					</row>
+					<row>
+						<entry>useSsl</entry>
+						<entry>
+                        Whether or not the LDAP client should use SSL encrypted transport. The <code>useSsl</code>
+                        and <code>useStartTls</code> options are mutually exclusive.
+                    </entry>
+					</row>
+					<row>
+						<entry>username</entry>
+						<entry>
+                        The default credentials username. Some servers require that this be in DN form. This must be given in DN form if the LDAP server requires a DN to bind and binding should be possible with simple usernames.
+                    </entry>
+					</row>
+					<row>
+						<entry>password</entry>
+						<entry>
+                        The default credentials password (used only with username above).
+                    </entry>
+					</row>
+					<row>
+						<entry>bindRequiresDn</entry>
+						<entry>
+                        If <code>true</code>, this instructs <code>Zend_Ldap</code> to retrieve the DN for the
+                        account used to bind if the username is not already in DN form. The default value is
+                        <code>false</code>.
+                    </entry>
+					</row>
+					<row>
+						<entry>baseDn</entry>
+						<entry>
+                        The default base DN used for searching (e.g., for accounts). This option is required for
+                        most account related operations and should indicate the DN under which accounts are
+                        located.
+                    </entry>
+					</row>
+					<row>
+						<entry>accountCanonicalForm</entry>
+						<entry>
+                        A small integer indicating the form to which account names should be canonicalized. See the <link linkend="zend.ldap.introduction.theory-of-operations.account-name-canonicalization">
+								<emphasis>Account Name Canonicalization</emphasis>
+							</link>
+                        section below.
+                    </entry>
+					</row>
+					<row>
+						<entry>accountDomainName</entry>
+						<entry>
+                        The FQDN domain for which the target LDAP server is an authority (e.g., example.com).
+                    </entry>
+					</row>
+					<row>
+						<entry>accountDomainNameShort</entry>
+						<entry>
+                        The 'short' domain for which the target LDAP server is an authority. This is usually used
+                        to specify the NetBIOS domain name for Windows networks but may also be used by non-AD
+                        servers.
+                    </entry>
+					</row>
+					<row>
+						<entry>accountFilterFormat</entry>
+						<entry>
+                        The LDAP search filter used to search for accounts. This string is a
+                        <ulink url="http://php.net/sprintf">
+								<code>sprintf()</code>
+							</ulink> style expression that must
+                        contain one '<code>%s</code>' to accommodate the username. The default value is
+                        '<code>(&amp;(objectClass=user)(sAMAccountName=%s))</code>' unless
+                        <code>bindRequiresDn</code> is set to <code>true</code>, in which case the default is
+                        '<code>(&amp;(objectClass=posixAccount)(uid=%s))</code>'. Users of custom schemas may need
+                        to change this option.
+                    </entry>
+					</row>
+					<row>
+						<entry>allowEmptyPassword</entry>
+						<entry>
+                        Some LDAP servers can be configured to accept an empty string
+                        password as an anonymous bind. This behavior is almost always
+                        undesirable. For this reason, empty passwords are explicitly
+                        disallowed. Set this value to <code>true</code> to allow an
+                        empty string password to be submitted during the bind.
+                    </entry>
+					</row>
+					<row>
+						<entry>optReferrals</entry>
+						<entry>
+                        If set to <code>true</code>, this option indicates to the LDAP client that referrals should
+                        be followed. The default value is <code>false</code>.
+                    </entry>
+					</row>
+					<row>
+						<entry>tryUsernameSplit</entry>
+						<entry>
+                        If set to <code>false</code>, this option indicates that the given username should not be split at the first <code>@</code> or <code>\</code> character to seperate the username from the domain during the binding-procedure. This allows the user to use usernames that contain an <code>@</code> or <code>\</code> character that do not inherit some domain-information, e.g. using email-addresses for binding. The default value is <code>true</code>.
+                    </entry>
+					</row>
+				</tbody>
+			</tgroup>
+		</table>
+	</sect2>
+	<sect2 id="zend.ldap.api.reference">
+		<title>API Reference</title>
+		<note>
+			<para>Method names in <emphasis>italics</emphasis> are static methods.</para>
+		</note>
+		<sect3 id="zend.ldap.api.reference.zend-ldap">
+			<title>Zend_Ldap</title>
+			<para>
+				<code>Zend_Ldap</code> is the base interface into a LDAP server. It provides connection and binding methods as well as methods to operate on the LDAP tree.</para>
+			<table id="zend.ldap.api.reference.zend-ldap.table">
+				<title>Zend_Ldap API</title>
+				<tgroup cols="2">
+					<thead>
+						<row>
+							<entry>Method</entry>
+							<entry>Description</entry>
+						</row>
+					</thead>
+					<tbody>
+						<row>
+							<entry>
+								<emphasis>
+									<code>string filterEscape(string $str)</code>
+								</emphasis>
+							</entry>
+							<entry>Escapes a value to be used in a LDAP filter according to RFC 2254. This method is <emphasis>deprecated</emphasis>, please use <code>Zend_Ldap_Filter_Abstract::escapeValue()</code> instead.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>boolean explodeDn($dn, array &amp;$keys = null, array &amp;$vals = null)</code>
+								</emphasis>
+							</entry>
+							<entry>Checks if a given DN <code>$dn</code> is malformed. If <code>$keys</code> or <code>$keys</code> and <code>$vals</code> are given, these arrays will be filled with the appropriate DN keys and values. This method is <emphasis>deprecated</emphasis>, please use <code>Zend_Ldap_Dn::checkDn()</code> instead.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>__construct($options)</code>
+							</entry>
+							<entry>Constructor. The <code>$options</code> parameter is optional and can be set to an array or a <code>Zend_Config</code> instance. If no options are provided at instantiation, the connection parameters must be passed to the instance using <code>Zend_Ldap::setOptions()</code>. The allowed options are specified in <link linkend="zend.ldap.using.theory-of-operation.options.table">Zend_Ldap Options</link>
+							</entry>
+						</row>
+						<row>
+							<entry>
+								<code>resource getResource()</code>
+							</entry>
+							<entry>Returns the raw LDAP extension (ext/ldap) resource.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>integer getLastErrorCode()</code>
+							</entry>
+							<entry>Returns the LDAP error number of the last LDAP command.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string getLastError(integer &amp;$errorCode, array &amp;$errorMessages)</code>
+							</entry>
+							<entry>Returns the LDAP error message of the last LDAP command. The optional <code>$errorCode</code> parameter is set to the LDAP error number when given. The optional <code>$errorMessages</code> array will be filled with the raw error messages when given. The various LDAP error retrieval functions can return different things, so they are all collected if <code>$errorMessages</code> is given.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap setOptions($options)</code>
+							</entry>
+							<entry>Sets the LDAP connection and binding parameters. <code>$options</code> can be an array or an instance of <code>Zend_Config</code>. The allowed options are specified in <link linkend="zend.ldap.using.theory-of-operation.options.table">Zend_Ldap Options</link>
+							</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getOptions()</code>
+							</entry>
+							<entry>Returns the current connection and binding parameters.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string getBaseDn()</code>
+							</entry>
+							<entry>Returns the base DN this LDAP connection is bound to.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string getCanonicalAccountName(string $acctname, integer $form)</code>
+							</entry>
+							<entry>Returns the canonical account name of the given account name <code>$acctname</code>. <code>$form</code> specifies the <link linkend="zend.ldap.using.theory-of-operation.account-name-canonicalization.table">format</link> into which the account name is canonicalized. See <link linkend="zend.ldap.introduction.theory-of-operations.account-name-canonicalization">Account Name Canonicalization</link> for more details.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap disconnect()</code>
+							</entry>
+							<entry>Disconnects the Zend_Ldap instance from the LDAP server.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap connect(string $host, integer $port, boolean $useSsl, boolean $useStartTls)</code>
+							</entry>
+							<entry>Connects the Zend_Ldap instance to the given LDAP server. All parameters are optional and will be taken from the LDAP connection and binding parameters passed to the instance via the construtor or via <code>Zend_Ldap::setOptions()</code> when set to <code>null</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap bind(string $username, string $password)</code>
+							</entry>
+							<entry>Authenticates <code>$username</code> with <code>$password</code> at the LDAP server. If both paramaters are omitted the binding will be carried out with the credentials given in the connection and binding parameters. If no credentials are given in the connection and binding parameters an anonymous bind will be performed. Note that this requires anonymous binds to be allowed on the LDAP server. An empty string <code>''</code> can be passed as <code>$password</code> together with a username if, and only if, <code>allowEmptyPassword</code> is set to <code>true</code> in the connection and binding parameters.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Collection search(string|Zend_Ldap_Filter_Abstract $filter, string|Zend_Ldap_Dn $basedn, integer $scope, array $attributes, string $sort, string $collectionClass)</code>
+							</entry>
+							<entry>Searches the LDAP tree with the given <code>$filter</code> and the given search parameters.
+                    <variablelist>
+									<varlistentry>
+										<term>
+											<code>string|Zend_Ldap_Filter_Abstract $filter</code>
+										</term>
+										<listitem>The filter string to be used in the search, e.g. <code>(objectClass=posixAccount)</code>.</listitem>
+									</varlistentry>
+									<varlistentry>
+										<term>
+											<code>string|Zend_Ldap_Dn $basedn</code>
+										</term>
+										<listitem>The search base for the search. If omitted or <code>null</code>, the <code>baseDn</code> from the connection and binding parameters is used.</listitem>
+									</varlistentry>
+									<varlistentry>
+										<term>
+											<code>integer $scope</code>
+										</term>
+										<listitem>The search scope. <code>Zend_Ldap::SEARCH_SCOPE_SUB</code> searches the complete subtree including the <code>$baseDn</code> node. <code>Zend_Ldap::SEARCH_SCOPE_ONE</code> restricts search to one level below <code>$baseDn</code>. <code>Zend_Ldap::SEARCH_SCOPE_BASE</code> restricts search to the <code>$baseDn</code> itself; this can be used to efficiently retrieve a single entry by its DN. The default value is <code>Zend_Ldap::SEARCH_SCOPE_SUB</code>.</listitem>
+									</varlistentry>
+									<varlistentry>
+										<term>
+											<code>array $attributes</code>
+										</term>
+										<listitem>Specifies the attributes contained in the returned entries. To include all possible attributes (ACL restrictions can disallow certain attribute to be retrieved by a given user) pass either an empty array <code>array()</code> or <code>array('*')</code> to the method. On some LDAP servers you can retrieve special internal attributes by passing <code>array('*', '+')</code> to the method.</listitem>
+									</varlistentry>
+									<varlistentry>
+										<term>
+											<code>string $sort</code>
+										</term>
+										<listitem>If given the result collection will be sorted after the attribute <code>$sort</code>. Results can only be sorted after one single attribute as this parameter uses the ext/ldap function <code>ldap_sort()</code>.</listitem>
+									</varlistentry>
+									<varlistentry>
+										<term>
+											<code>string $collectionClass</code>
+										</term>
+										<listitem>If given the result will be wrapped in an object of type <code>$collectionClass</code>. By default an object of type <code>Zend_Ldap_Collection</code> will be returned. The custom class must extend <code>Zend_Ldap_Collection</code> and will be passed a <code>Zend_Ldap_Collection_Iterator_Default</code> on instantiation.</listitem>
+									</varlistentry>
+								</variablelist>
+							</entry>
+						</row>
+						<row>
+							<entry>
+								<code>integer count(string|Zend_Ldap_Filter_Abstract $filter, string|Zend_Ldap_Dn $basedn, integer $scope)</code>
+							</entry>
+							<entry>Counts the elements returned by the given search parameters. See <code>Zend_Ldap::search()</code> for a detailed description of the method parameters.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>integer countChildren(string|Zend_Ldap_Dn $dn)</code>
+							</entry>
+							<entry>Counts the direct descendants (children) of the entry identified by the given <code>$dn</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>boolean exists(string|Zend_Ldap_Dn $dn)</code>
+							</entry>
+							<entry>Checks whether the entry identified by the given <code>$dn</code> exists.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array searchEntries(string|Zend_Ldap_Filter_Abstract $filter, string|Zend_Ldap_Dn $basedn, integer $scope, array $attributes, string $sort)</code>
+							</entry>
+							<entry>Performs a search operation and returns the result as an PHP array. This is essentially the same method as <code>Zend_Ldap::search()</code> except for the return type. See <code>Zend_Ldap::search()</code> for a detailed description of the method parameters.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getEntry(string|Zend_Ldap_Dn $dn, array $attributes, boolean $throwOnNotFound)</code>
+							</entry>
+							<entry>Retrieves the LDAP entry identified by <code>$dn</code> with the attributes specified in <code>$attributes</code>. If <code>$attributes</code> is ommitted, all attributes (<code>array()</code>) are included in the result. <code>$throwOnNotFound</code> is <code>false</code> by default, so the method will return <code>null</code> if the specified entry cannot be found. If set to <code>true</code>, a <code>Zend_Ldap_Exception</code> will be thrown instead.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>void prepareLdapEntryArray(array &amp;$entry)</code>
+								</emphasis>
+							</entry>
+							<entry>Prepare an array for the use in LDAP modification operations. This method does not need to be called by the end-user as it's implicitly called on every data modification method.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap add(string|Zend_Ldap_Dn $dn, array $entry)</code>
+							</entry>
+							<entry>Adds the entry identified by <code>$dn</code> with its attributes <code>$entry</code> to the LDAP tree. Throws a <code>Zend_Ldap_Exception</code> if the entry could not be added.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap update(string|Zend_Ldap_Dn $dn, array $entry)</code>
+							</entry>
+							<entry>Updates the entry identified by <code>$dn</code> with its attributes <code>$entry</code> to the LDAP tree. Throws a <code>Zend_Ldap_Exception</code> if the entry could not be modified.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap save(string|Zend_Ldap_Dn $dn, array $entry)</code>
+							</entry>
+							<entry>Saves the entry identified by <code>$dn</code> with its attributes <code>$entry</code> to the LDAP tree. Throws a <code>Zend_Ldap_Exception</code> if the entry could not be saved. This method decides by querying the LDAP tree if the entry will be added or updated.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap delete(string|Zend_Ldap_Dn $dn, boolean $recursively)</code>
+							</entry>
+							<entry>Deletes the entry identified by <code>$dn</code> from the LDAP tree. Throws a <code>Zend_Ldap_Exception</code> if the entry could not be deleted. <code>$recursively</code> is <code>false</code> by default. If set to <code>true</code> the deletion will be carried out recursively and will effectively delete a complete subtree. Deletion will fail if <code>$recursively</code> is <code>false</code> and the entry <code>$dn</code> is not a leaf entry.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap moveToSubtree(string|Zend_Ldap_Dn $from, string|Zend_Ldap_Dn $to, boolean $recursively, boolean $alwaysEmulate)</code>
+							</entry>
+							<entry>Moves the entry identified by <code>$from</code> to a location below <code>$to</code> keeping its RDN unchanged. <code>$recursively</code> specifies if the operation will be carried out recursively (<code>false</code> by default) so that the entry <code>$from</code> and all its descendants will be moved. Moving will fail if <code>$recursively</code> is <code>false</code> and the entry <code>$from</code> is not a leaf entry. <code>$alwaysEmulate</code> controls whether the ext/ldap function <code>ldap_rename()</code> should be used if available. This can only work for leaf entries and for servers and for ext/ldap supporting this function. Set to <code>true</code> to always use an emulated rename operation.
+                    <note>
+									<para>All move-operations are carried out by copying and then deleting the corresponding entries in the LDAP tree. These operations are not <emphasis>atomic</emphasis> so that failures during the operation will result in an <emphasis>inconsistent</emphasis> state on the LDAP server. The same is true for all recursive operations. They also are by no means atomic. Please keep this in mind.</para>
+								</note>
+							</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap move(string|Zend_Ldap_Dn $from, string|Zend_Ldap_Dn $to, boolean $recursively, boolean $alwaysEmulate)</code>
+							</entry>
+							<entry>This is an alias for <code>Zend_Ldap::rename()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap rename(string|Zend_Ldap_Dn $from, string|Zend_Ldap_Dn $to, boolean $recursively, boolean $alwaysEmulate)</code>
+							</entry>
+							<entry>Renames the entry identified by <code>$from</code> to <code>$to</code>. <code>$recursively</code> specifies if the operation will be carried out recursively (<code>false</code> by default) so that the entry <code>$from</code> and all its descendants will be moved. Moving will fail if <code>$recursively</code> is <code>false</code> and the entry <code>$from</code> is not a leaf entry. <code>$alwaysEmulate</code> controls whether the ext/ldap function <code>ldap_rename()</code> should be used if available. This can only work for leaf entries and for servers and for ext/ldap supporting this function. Set to <code>true</code> to always use an emulated rename operation.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap copyToSubtree(string|Zend_Ldap_Dn $from, string|Zend_Ldap_Dn $to, boolean $recursively)</code>
+							</entry>
+							<entry>Copies the entry identified by <code>$from</code> to a location below <code>$to</code> keeping its RDN unchanged. <code>$recursively</code> specifies if the operation will be carried out recursively (<code>false</code> by default) so that the entry <code>$from</code> and all its descendants will be copied. Copying will fail if <code>$recursively</code> is <code>false</code> and the entry <code>$from</code> is not a leaf entry.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap copy(string|Zend_Ldap_Dn $from, string|Zend_Ldap_Dn $to, boolean $recursively)</code>
+							</entry>
+							<entry>Copies the entry identified by <code>$from</code> to <code>$to</code>. <code>$recursively</code> specifies if the operation will be carried out recursively (<code>false</code> by default) so that the entry <code>$from</code> and all its descendants will be copied. Copying will fail if <code>$recursively</code> is <code>false</code> and the entry <code>$from</code> is not a leaf entry.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node getNode(string|Zend_Ldap_Dn $dn)</code>
+							</entry>
+							<entry>Returns the entry <code>$dn</code> wrapped in a <code>Zend_Ldap_Node</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node getBaseNode()</code>
+							</entry>
+							<entry>Returns the entry for the base DN <code>$baseDn</code> wrapped in a <code>Zend_Ldap_Node</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node_RootDse getRootDse()</code>
+							</entry>
+							<entry>Returns the RootDSE for the current server.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node_Schema getSchema()</code>
+							</entry>
+							<entry>Returns the LDAP schema for the current server.</entry>
+						</row>
+					</tbody>
+				</tgroup>
+			</table>
+			<sect4 id="zend.ldap.api.reference.zend-ldap.zend-ldap-collection">
+				<title>Zend_Ldap_Collection</title>
+				<para>
+					<code>Zend_Ldap_Collection</code> implements <code>Iterator</code> to allow for item traversal using <code>foreach()</code> and <code>Countable</code> to be able to respond to <code>count()</code>. With its protected <code>_createEntry()</code> method it provides a simple extension point for developers needing custom result objects.</para>
+				<table id="zend.ldap.api.reference.zend-collection.table">
+					<title>Zend_Ldap_Collection API</title>
+					<tgroup cols="2">zend.ldap.api.reference.zend-ldap.zend-ldap-collection
+						<thead>
+							<row>
+								<entry>Method</entry>
+								<entry>Description</entry>
+							</row>
+						</thead>
+						<tbody>
+							<row>
+								<entry>
+									<code>__construct(Zend_Ldap_Collection_Iterator_Interface $iterator)</code>
+								</entry>
+								<entry>Constructor. The constrcutor must be provided by a <code>Zend_Ldap_Collection_Iterator_Interface</code> which does the real result iteration. <code>Zend_Ldap_Collection_Iterator_Default</code> is the default implementation for iterating ext/ldap results.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>boolean close()</code>
+								</entry>
+								<entry>Closes the internal iterator. This is also called in the destructor.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>array toArray()</code>
+								</entry>
+								<entry>Returns all entries as an array.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>array getFirst()</code>
+								</entry>
+								<entry>Returns the first entry in the collection or <code>null</code> if the collection is empty.</entry>
+							</row>
+						</tbody>
+					</tgroup>
+				</table>
+			</sect4>
+		</sect3>
+		<sect3 id="zend.ldap.api.reference.zend-ldap-attribute">
+			<title>Zend_Ldap_Attribute</title>
+			<para>
+				<code>Zend_Ldap_Attribute</code> is a helper class providing only static methods to manipulate arrays suitable to the structure used in <code>Zend_Ldap</code> data modification methods and to the data format required by the LDAP server. PHP data types are converted the following way:</para>
+			<variablelist>
+				<varlistentry>
+					<term>
+						<code>string</code>
+					</term>
+					<listitem>No conversion will be done.</listitem>
+				</varlistentry>
+				<varlistentry>
+					<term>
+						<code>integer</code> and <code>float</code>
+					</term>
+					<listitem>The value will be converted to a string.</listitem>
+				</varlistentry>
+				<varlistentry>
+					<term>
+						<code>boolean</code>
+					</term>
+					<listitem>
+						<code>true</code> will be converted to <code>'TRUE'</code> and <code>false</code> to <code>'FALSE'</code>
+					</listitem>
+				</varlistentry>
+				<varlistentry>
+					<term>
+						<code>object</code> and <code>array</code>
+					</term>
+					<listitem>The value will be converted to a string by using <code>serialize()</code>.</listitem>
+				</varlistentry>
+				<varlistentry>
+					<term>
+						<code>resource</code>
+					</term>
+					<listitem>If a <code>stream</code> resource is given, the data will be fetched by calling <code>stream_get_contents()</code>.</listitem>
+				</varlistentry>
+				<varlistentry>
+					<term>others</term>
+					<listitem>All other data types (namely non-stream resources) will be ommitted.</listitem>
+				</varlistentry>
+			</variablelist>
+			<para>On reading attribute values the following conversion will take place:</para>
+			<variablelist>
+				<varlistentry>
+					<term>
+						<code>'TRUE'</code>
+					</term>
+					<listitem>Converted to <code>true</code>.</listitem>
+				</varlistentry>
+				<varlistentry>
+					<term>
+						<code>'FALSE'</code>
+					</term>
+					<listitem>Converted to <code>false</code>.</listitem>
+				</varlistentry>
+				<varlistentry>
+					<term>others</term>
+					<listitem>All other strings won't be automatically converted and are passed as they are.</listitem>
+				</varlistentry>
+			</variablelist>
+			<table id="zend.ldap.api.reference.zend-ldap-attribute">
+				<title>Zend_Ldap_Attribute API</title>
+				<tgroup cols="2">
+					<thead>
+						<row>
+							<entry>Method</entry>
+							<entry>Description</entry>
+						</row>
+					</thead>
+					<tbody>
+						<row>
+							<entry>
+								<emphasis>
+									<code>void setAttribute(array &amp;$data, string $attribName, mixed $value, boolean $append)</code>
+								</emphasis>
+							</entry>
+							<entry>Sets the attribute <code>$attribName</code> in <code>$data</code> to the value <code>$value</code>. If <code>$append</code> is <code>true</code> (<code>false</code> by default) <code>$value</code> will be appended to the attribute. <code>$value</code> can be a scalar value or an array of scalar values. Conversion will take place.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>array|mixed getAttribute(array $data, string $attribName, integer|null $index)</code>
+								</emphasis>
+							</entry>
+							<entry>Returns the attribute <code>$attribName</code> from <code>$data</code>. If <code>$index</code> is <code>null</code> (default) an array will be returned containing all the values for the given attribute. An empty array will be returned if the attribute does not exist in the given array. If an integer index is specified the corresponding value at the given index will be returned. If the index is out of bounds, <code>null</code> will be returned. Conversion will take place.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>boolean attributeHasValue(array &amp;$data, string $attribName, mixed|array $value)</code>
+								</emphasis>
+							</entry>
+							<entry>Checks if the attribute <code>$attribName</code> in <code>$data</code> has the value(s) given in <code>$value</code>. The method returns <code>true</code> only if all values in <code>$value</code> are present in the attribute. Comparison is done strictly (respecting the data type).</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>void removeDuplicatesFromAttribute(array &amp;$data, string $attribName)</code>
+								</emphasis>
+							</entry>
+							<entry>Removes all duplicates from the attribute <code>$attribName</code> in <code>$data</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>void removeFromAttribute(array &amp;$data, string $attribName, mixed|array $value)</code>
+								</emphasis>
+							</entry>
+							<entry>Removes the the value(s) given in <code>$value</code> from the attribute <code>$attribName</code> in <code>$data</code>. </entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>string|null convertToLdapValue(mixed $value)</code>
+								</emphasis>
+							</entry>
+							<entry>Converts a PHP data type into its LDAP representation. See introduction for details.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>mixed convertFromLdapValue(string $value)</code>
+								</emphasis>
+							</entry>
+							<entry>Converts an LDAP value into its PHP data type. See introduction for details.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>string|null convertToLdapDateTimeValue(integer $value, boolean $utc)</code>
+								</emphasis>
+							</entry>
+							<entry>Converts a timestamp into its LDAP date/time representation. If <code>$utc</code> is <code>true</code> (<code>false</code> by default) the resulting LDAP date/time string will be in UTC, otherwise a local date/time string will be returned.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>integer|null convertFromLdapDateTimeValue(string $value)</code>
+								</emphasis>
+							</entry>
+							<entry>Converts LDAP date/time representation into a timestamp. The method returns <code>null</code> if <code>$value</code> can not be converted back into a PHP timestamp.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>void setPassword(array &amp;$data, string $password, string $hashType, string $attribName)</code>
+								</emphasis>
+							</entry>
+							<entry>Sets a LDAP password for the the attribute <code>$attribName</code> in <code>$data</code>. <code>$attribName</code> defaults to <code>'userPassword'</code> which is the standard password attribute. The password hash can be specified with <code>$hashType</code>. The default value here is <code>Zend_Ldap_Attribute::PASSWORD_HASH_MD5</code> with <code>Zend_Ldap_Attribute::PASSWORD_HASH_SHA</code> as the other possibilty.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>string createPassword(string $password, string $hashType)</code>
+								</emphasis>
+							</entry>
+							<entry>Creates a LDAP password. The password hash can be specified with <code>$hashType</code>. The default value here is <code>Zend_Ldap_Attribute::PASSWORD_HASH_MD5</code> with <code>Zend_Ldap_Attribute::PASSWORD_HASH_SHA</code> as the other possibilty.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>void setDateTimeAttribute(array &amp;$data, string $attribName, integer|array $value, boolean $utc, boolean $append)</code>
+								</emphasis>
+							</entry>
+							<entry>Sets the attribute <code>$attribName</code> in <code>$data</code> to the date/time value <code>$value</code>. If <code>$append</code> is <code>true</code> (<code>false</code> by default) <code>$value</code> will be appended to the attribute. <code>$value</code> can be an integer value or an array of integers. Date-time-conversion according to <code>Zend_Ldap_Attribute::convertToLdapDateTimeValue()</code> will take place.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>array|integer getDateTimeAttribute(array $data, string $attribName, integer|null $index)</code>
+								</emphasis>
+							</entry>
+							<entry>Returns the date/time attribute <code>$attribName</code> from <code>$data</code>. If <code>$index</code> is <code>null</code> (default) an array will be returned containing all the date/time values for the given attribute. An empty array will be returned if the attribute does not exist in the given array. If an integer index is specified the corresponding date/time value at the given index will be returned. If the index is out of bounds, <code>null</code> will be returned. Date-time-conversion according to <code>Zend_Ldap_Attribute::convertFromLdapDateTimeValue()</code> will take place.</entry>
+						</row>
+					</tbody>
+				</tgroup>
+			</table>
+		</sect3>
+		<sect3 id="zend.ldap.api.reference.zend-ldap-dn">
+			<title>Zend_Ldap_Dn</title>
+			<para>
+				<code>Zend_Ldap_Dn</code> provides an object-oriented interface to manipulating LDAP distinguished names (DN). The parameter <code>$caseFold</code> that is used in several methods determines the way DN attributes are handled regarding their case. Allowed values for this paraneter are:</para>
+			<variablelist>
+				<varlistentry>
+					<term>
+						<code>Zend_Ldap_Dn::ATTR_CASEFOLD_NONE</code>
+					</term>
+					<listitem>No case-folding will be done.</listitem>
+				</varlistentry>
+				<varlistentry>
+					<term>
+						<code>Zend_Ldap_Dn::ATTR_CASEFOLD_UPPER</code>
+					</term>
+					<listitem>All attributes will be converted to upper-case.</listitem>
+				</varlistentry>
+				<varlistentry>
+					<term>
+						<code>Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER</code>
+					</term>
+					<listitem>All attributes will be converted to lower-case.</listitem>
+				</varlistentry>
+			</variablelist>
+			<para>The default case-folding is <code>Zend_Ldap_Dn::ATTR_CASEFOLD_NONE</code> and can be set with <code>Zend_Ldap_Dn::setDefaultCaseFold()</code>. Each instance of <code>Zend_Ldap_Dn</code> can have its own case-folding-setting. If the <code>$caseFold</code> parameter is ommitted in method-calls it defaults to the instance's case-folding setting.</para>
+			<para>The class implements <code>ArrayAccess</code> to allow indexer-access to the different parts of the DN. The <code>ArrayAccess</code>-methods proxy to <code>Zend_Ldap_Dn::get($offset, 1, null)</code> for <code>offsetGet(integer $offset)</code>, to <code>Zend_Ldap_Dn::set($offset, $value)</code> for <code>offsetSet()</code> and to <code>Zend_Ldap_Dn::remove($offset, 1)</code> for <code>offsetUnset()</code>. <code>offsetExists()</code> simply checks if the index is within the bounds.</para>
+			<table id="zend.ldap.api.reference.zend-ldap-dn">
+				<title>Zend_Ldap_Dn API</title>
+				<tgroup cols="2">
+					<thead>
+						<row>
+							<entry>Method</entry>
+							<entry>Description</entry>
+						</row>
+					</thead>
+					<tbody>
+						<row>
+							<entry>
+								<emphasis>
+									<code>Zend_Ldap_Dn factory(string|array $dn, string|null $caseFold)</code>
+								</emphasis>
+							</entry>
+							<entry>Creates a <code>Zend_Ldap_Dn</code> instance from an array or a string. The array must conform to the array structure detailed under <code>Zend_Ldap_Dn::implodeDn()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>Zend_Ldap_Dn fromString(string $dn, string|null $caseFold)</code>
+								</emphasis>
+							</entry>
+							<entry>Creates a <code>Zend_Ldap_Dn</code> instance from a string.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>Zend_Ldap_Dn fromArray(array $dn, string|null $caseFold)</code>
+								</emphasis>
+							</entry>
+							<entry>Creates a <code>Zend_Ldap_Dn</code> instance from an array. The array must conform to the array structure detailed under <code>Zend_Ldap_Dn::implodeDn()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getRdn(string|null $caseFold)</code>
+							</entry>
+							<entry>Gets the RDN of the current DN. The return value is an array with the RDN attribute names its keys and the RDN attribute values.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string getRdnString(string|null $caseFold)</code>
+							</entry>
+							<entry>Gets the RDN of the current DN. The return value is a string.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Dn getParentDn(integer $levelUp)</code>
+							</entry>
+							<entry>Gets the DN of the current DN's ancestor <code>$levelUp</code> levels up the tree. <code>$levelUp</code> defaults to <code>1</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array get(integer $index, integer $length, string|null $caseFold)</code>
+							</entry>
+							<entry>Returns a slice of the current DN determined by <code>$index</code> and <code>$length</code>. <code>$index</code> starts with <code>0</code> on the DN part from the left.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Dn set(integer $index, array $value)</code>
+							</entry>
+							<entry>Replaces a DN part in the current DN. This operation manipulates the current instance.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Dn remove(integer $index, integer $length)</code>
+							</entry>
+							<entry>Removes a DN part from the current DN. This operation manipulates the current instance. <code>$length</code> defaults to <code>1</code>
+							</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Dn append(array $value)</code>
+							</entry>
+							<entry>Appends a DN part to the current DN. This operation manipulates the current instance.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Dn prepend(array $value)</code>
+							</entry>
+							<entry>Prepends a DN part to the current DN. This operation manipulates the current instance.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Dn insert(integer $index, array $value)</code>
+							</entry>
+							<entry>Inserts a DN part after the index <code>$index</code> to the current DN. This operation manipulates the current instance.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>void setCaseFold(string|null $caseFold)</code>
+							</entry>
+							<entry>Sets the case-folding option to the current DN instance. If <code>$caseFold</code> is null the default case-folding setting (<code>Zend_Ldap_Dn::ATTR_CASEFOLD_NONE</code> by default or set via <code>Zend_Ldap_Dn::setDefaultCaseFold()</code> will be set for the current instance.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string toString(string|null $caseFold)</code>
+							</entry>
+							<entry>Returns DN as a string.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array toArray(string|null $caseFold)</code>
+							</entry>
+							<entry>Returns DN as an array.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string __toString()</code>
+							</entry>
+							<entry>Returns DN as a string - proxies to <code>Zend_Ldap_Dn::toString(null)</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>void setDefaultCaseFold(string $caseFold)</code>
+								</emphasis>
+							</entry>
+							<entry>Sets the default case-folding option used by all instances on creation by default. Already existing instances are not affected by this setting.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>array escapeValue(string|array $values)</code>
+								</emphasis>
+							</entry>
+							<entry>Escapes a DN value according to RFC 2253.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>array unescapeValue(string|array $values)</code>
+								</emphasis>
+							</entry>
+							<entry>Undoes the conversion done by <code>Zend_Ldap_Dn::escapeValue()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>array explodeDn(string $dn, array &amp;$keys, array &amp;$vals, string|null $caseFold)</code>
+								</emphasis>
+							</entry>
+							<entry>Explodes the DN <code>$dn</code> into an array containing all parts of the given DN. <code>$keys</code> optinally receive DN keys (e.g. CN, OU, DC, ...). <code>$vals</code> optionally receive DN values. The resulting array will be of type
+                    <programlisting role="php"><![CDATA[
+array(
+    array("cn" => "name1", "uid" => "user"),
+    array("cn" => "name2"),
+    array("dc" => "example"),
+    array("dc" => "org")
+)
+]]></programlisting> for a DN of <code>cn=name1+uid=user,cn=name2,dc=example,dc=org</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>boolean checkDn(string $dn, array &amp;$keys, array &amp;$vals, string|null $caseFold)</code>
+								</emphasis>
+							</entry>
+							<entry>Checks if a given DN <code>$dn</code> is malformed. If <code>$keys</code> or <code>$keys</code> and <code>$vals</code> are given, these arrays will be filled with the appropriate DN keys and values.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>string implodeRdn(array $part, string|null $caseFold)</code>
+								</emphasis>
+							</entry>
+							<entry>Returns a DN part in the form <code>$attribute=$value</code>
+							</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>string implodeDn(array $dnArray, string|null $caseFold, string $separator)</code>
+								</emphasis>
+							</entry>
+							<entry>Implodes an array in the form delivered by <code>Zend_Ldap_Dn::explodeDn()</code> to a DN string. <code>$separator</code> defaults to <code>','</code> but some LDAP servers also understand <code>';'</code>. <code>$dnArray</code> must of type
+                    <programlisting role="php"><![CDATA[
+array(
+    array("cn" => "name1", "uid" => "user"),
+    array("cn" => "name2"),
+    array("dc" => "example"),
+    array("dc" => "org")
+)
+]]></programlisting>
+							</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>boolean isChildOf(string|Zend_Ldap_Dn $childDn, string|Zend_Ldap_Dn $parentDn)</code>
+								</emphasis>
+							</entry>
+							<entry>Checks if given <code>$childDn</code> is beneath <code>$parentDn</code> subtree.</entry>
+						</row>
+					</tbody>
+				</tgroup>
+			</table>
+		</sect3>
+		<sect3 id="zend.ldap.api.reference.zend-ldap-filter">
+			<title>Zend_Ldap_Filter</title>
+			<table id="zend.ldap.api.reference.zend-filter.table">
+				<title>Zend_Ldap_Filter API</title>
+				<tgroup cols="2">
+					<thead>
+						<row>
+							<entry>Method</entry>
+							<entry>Description</entry>
+						</row>
+					</thead>
+					<tbody>
+						<row>
+							<entry>
+								<emphasis>
+									<code>Zend_Ldap_Filter equals(string $attr, string $value)</code>
+								</emphasis>
+							</entry>
+							<entry>Creates an 'equals' filter: <code>(attr=value)</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>Zend_Ldap_Filter begins(string $attr, string $value)</code>
+								</emphasis>
+							</entry>
+							<entry>Creates an 'begins with' filter: <code>(attr=value*)</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>Zend_Ldap_Filter ends(string $attr, string $value)</code>
+								</emphasis>
+							</entry>
+							<entry>Creates an 'ends with' filter: <code>(attr=*value)</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>Zend_Ldap_Filter contains(string $attr, string $value)</code>
+								</emphasis>
+							</entry>
+							<entry>Creates an 'contains' filter: <code>(attr=*value*)</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>Zend_Ldap_Filter greater(string $attr, string $value)</code>
+								</emphasis>
+							</entry>
+							<entry>Creates an 'greater' filter: <code>(attr&gt;value)</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>Zend_Ldap_Filter greaterOrEqual(string $attr, string $value)</code>
+								</emphasis>
+							</entry>
+							<entry>Creates an 'greater or equal' filter: <code>(attr&gt;=value)</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>Zend_Ldap_Filter less(string $attr, string $value)</code>
+								</emphasis>
+							</entry>
+							<entry>Creates an 'less' filter: <code>(attr&lt;value)</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>Zend_Ldap_Filter lessOrEqual(string $attr, string $value)</code>
+								</emphasis>
+							</entry>
+							<entry>Creates an 'less or equal' filter: <code>(attr&lt;=value)</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>Zend_Ldap_Filter approx(string $attr, string $value)</code>
+								</emphasis>
+							</entry>
+							<entry>Creates an 'approx' filter: <code>(attr~=value)</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>Zend_Ldap_Filter any(string $attr)</code>
+								</emphasis>
+							</entry>
+							<entry>Creates an 'any' filter: <code>(attr=*)</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>Zend_Ldap_Filter string(string $filter)</code>
+								</emphasis>
+							</entry>
+							<entry>Creates a simple custom string filter. The user is responsible for all value-escaping as the filter is used as is.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>Zend_Ldap_Filter mask(string $mask, string $value,...)</code>
+								</emphasis>
+							</entry>
+							<entry>Creates a filter from a string mask. All <code>$value</code> parameters will be escaped and substituted into <code>$mask</code> by using <ulink url="http://php.net/sprintf">
+									<code>sprintf()</code>
+								</ulink>
+							</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>Zend_Ldap_Filter andFilter(Zend_Ldap_Filter_Abstract $filter,...)</code>
+								</emphasis>
+							</entry>
+							<entry>Creates an 'and' filter from all arguments given.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>Zend_Ldap_Filter orFilter(Zend_Ldap_Filter_Abstract $filter,...)</code>
+								</emphasis>
+							</entry>
+							<entry>Creates an 'or' filter from all arguments given.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>__construct(string $attr, string $value, string $filtertype, string|null $prepend, string|null $append)</code>
+							</entry>
+							<entry>Constructor. Creates an arbitrary filter according to the parameters supplied. The resulting filter will be a concatenation <code>$attr . $filtertype . $prepend . $value . $append</code>. Normally this constructor is not needed as all filters can be created by using the apprpriate factory methods.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string toString()</code>
+							</entry>
+							<entry>Returns a string representation of the filter.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string __toString()</code>
+							</entry>
+							<entry>Returns a string representation of the filter. Proxies to <code>Zend_Ldap_Filter::toString()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Filter_Abstract negate()</code>
+							</entry>
+							<entry>Negates the current filter.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Filter_Abstract addAnd(Zend_Ldap_Filter_Abstract $filter,...)</code>
+							</entry>
+							<entry>Creates an 'and' filter from the current filter and all filters passed in as the arguments.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Filter_Abstract addOr(Zend_Ldap_Filter_Abstract $filter,...)</code>
+							</entry>
+							<entry>Creates an 'or' filter from the current filter and all filters passed in as the arguments.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>string|array escapeValue(string|array $values)</code>
+								</emphasis>
+							</entry>
+							<entry>Escapes the given <code>$values</code> according to RFC 2254 so that they can be safely used in LDAP filters. If a single string is given, a string is returned - otherwise an array is returned. Any control characters with an ACII code &lt; 32 as well as the characters with special meaning in LDAP filters "*", "(", ")", and "\" (the backslash) are converted into the representation of a backslash followed by two hex digits representing the hexadecimal value of the character.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>string|array unescapeValue(string|array $values)</code>
+								</emphasis>
+							</entry>
+							<entry>Undoes the conversion done by <code>Zend_Ldap_Filter::escapeValue()</code>. Converts any sequences of a backslash followed by two hex digits into the corresponding character.</entry>
+						</row>
+					</tbody>
+				</tgroup>
+			</table>
+		</sect3>
+		<sect3 id="zend.ldap.api.reference.zend-ldap-node">
+			<title>Zend_Ldap_Node</title>
+			<para>
+				<code>Zend_Ldap_Node</code> includes the magic propery accessors <code>__set()</code>, <code>__get()</code>, <code>__unset()</code> and <code>__isset()</code> to access the attributes by their name. They proxy to <code>Zend_Ldap_Node::setAttribute()</code>, <code>Zend_Ldap_Node::getAttribute()</code>, <code>Zend_Ldap_Node::deleteAttribute()</code> and <code>Zend_Ldap_Node::existsAttribute()</code> respectively. Furthermore the class implements <code>ArrayAccess</code> for array-style-access to the attributes. <code>Zend_Ldap_Node</code> also implements <code>Iterator</code> and <code>RecursiveIterato</code> to allow for recursive tree-traversal.</para>
+			<table id="zend.ldap.api.reference.zend-ldap-node.table">
+				<title>Zend_Ldap_Node API</title>
+				<tgroup cols="2">
+					<thead>
+						<row>
+							<entry>Method</entry>
+							<entry>Description</entry>
+						</row>
+					</thead>
+					<tbody>
+						<row>
+							<entry>
+								<code>Zend_Ldap getLdap()</code>
+							</entry>
+							<entry>Returns the current LDAP connection. Throws <code>Zend_Ldap_Exception</code> if current node is in detached mode (not connected to a <code>Zend_Ldap</code> instance).</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node attachLdap(Zend_Ldap $ldap)</code>
+							</entry>
+							<entry>Attach the current node to the <code>$ldap</code>
+								<code>Zend_Ldap</code> instance. Throws <code>Zend_Ldap_Exception</code> if <code>$ldap</code> is not responsible for the current node (node is not a child of the <code>$ldap</code> base DN).</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node detachLdap()</code>
+							</entry>
+							<entry>Detach node from LDAP connection.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>boolean isAttached()</code>
+							</entry>
+							<entry>Checks if the current node is attached to a LDAP connection.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>Zend_Ldap_Node create(string|array|Zend_Ldap_Dn $dn, array $objectClass)</code>
+								</emphasis>
+							</entry>
+							<entry>Factory method to create a new detached <code>Zend_Ldap_Node</code> for a given DN. Creates a new <code>Zend_Ldap_Node</code> with the DN <code>$dn</code> and the object-classes <code>$objectClass</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>Zend_Ldap_Node fromLdap(string|array|Zend_Ldap_Dn $dn, Zend_Ldap $ldap)</code>
+								</emphasis>
+							</entry>
+							<entry>Factory method to create an attached <code>Zend_Ldap_Node</code> for a given DN. Loads an existing <code>Zend_Ldap_Node</code> with the DN <code>$dn</code> from the LDAP connection <code>$ldap</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>Zend_Ldap_Node fromArray((array $data, boolean $fromDataSource)</code>
+								</emphasis>
+							</entry>
+							<entry>Factory method to create a detached <code>Zend_Ldap_Node</code> from array data <code>$data</code>. If <code>$fromDataSource</code> is <code>true</code> (<code>false</code> by default), the data is treated as beeing present in a LDAP tree.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>boolean isNew()</code>
+							</entry>
+							<entry>Tells if the node is consiedered as new (not present on the server). Please note, that this doesn't tell if the node is really present on the server. Use <code>Zend_Ldap_Node::exists()</code> to see if a node is already there.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>boolean willBeDeleted()</code>
+							</entry>
+							<entry>Tells if this node is going to be deleted once <code>Zend_Ldap_Node::update()</code> is called.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node delete()</code>
+							</entry>
+							<entry>Marks this node as to be deleted. Node will be deleted on calling <code>Zend_Ldap_Node::update()</code> if <code>Zend_Ldap_Node::willBeDeleted()</code> is true.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>boolean willBeMoved()</code>
+							</entry>
+							<entry>Tells if this node is going to be moved once <code>Zend_Ldap_Node::update()</code> is called.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node update(Zend_Ldap $ldap)</code>
+							</entry>
+							<entry>Sends all pending changes to the LDAP server. If <code>$ldap</code> is omitted the current LDAP connection is used. If the current node is detached from a LDAP connection a <code>Zend_Ldap_Exception</code> will be thrown. If <code>$ldap</code> is provided the current node will be attached to the given LDAP connection.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Dn getCurrentDn()</code>
+							</entry>
+							<entry>Gets the current DN of the current node as a Zend_Ldap_Dn. This does not reflect possible rename-operations.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Dn getDn()</code>
+							</entry>
+							<entry>Gets the original DN of the current node as a Zend_Ldap_Dn. This reflects possible rename-operations.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string getDnString(string $caseFold)</code>
+							</entry>
+							<entry>Gets the original DN of the current node as a string. This reflects possible rename-operations.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getDnArray(string $caseFold)</code>
+							</entry>
+							<entry>Gets the original DN of the current node as an array. This reflects possible rename-operations.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string getRdnString(string $caseFold)</code>
+							</entry>
+							<entry>Gets the RDN of the current node as a string. This reflects possible rename-operations.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getRdnArray(string $caseFold)</code>
+							</entry>
+							<entry>Gets the RDN of the current node as an array. This reflects possible rename-operations.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node setDn(Zend_Ldap_Dn|string|array $newDn)</code>
+							</entry>
+							<entry>Sets the new DN for this node effectively moving the node once <code>Zend_Ldap_Node::update()</code> is called.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node move(Zend_Ldap_Dn|string|array $newDn)</code>
+							</entry>
+							<entry>This is an alias for <code>Zend_Ldap_Node::setDn()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node rename(Zend_Ldap_Dn|string|array $newDn)</code>
+							</entry>
+							<entry>This is an alias for <code>Zend_Ldap_Node::setDn()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getObjectClass()</code>
+							</entry>
+							<entry>Returns the objectClass of the node.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node setObjectClass(array|string $value)</code>
+							</entry>
+							<entry>Sets the objectClass attribute.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node appendObjectClass(array|string $value)</code>
+							</entry>
+							<entry>Appends to the objectClass attribute.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string toLdif(array $options)</code>
+							</entry>
+							<entry>Returns a LDIF representation of the current node. <code>$options</code> will be passed to the <code>Zend_Ldap_Ldif_Encoder</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getChangedData()</code>
+							</entry>
+							<entry>Gets changed node data. The array contains all changed attributes. This format can be used in <code>Zend_Ldap::add()</code> and <code>Zend_Ldap::update()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getChanges()</code>
+							</entry>
+							<entry>Returns all changes made.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string toString()</code>
+							</entry>
+							<entry>Returns the DN of the current node - proxies to <code>Zend_Ldap_Dn::getDnString()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string __toString()</code>
+							</entry>
+							<entry>Casts to string representation - proxies to <code>Zend_Ldap_Dn::toString()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array toArray(boolean $includeSystemAttributes)</code>
+							</entry>
+							<entry>Returns an array representation of the current node. If <code>$includeSystemAttributes</code> is <code>false</code> (defaults to <code>true</code>) the system specific attributes are stripped from the array. Unlike <code>Zend_Ldap_Node::getAttributes()</code> the resulting array contains the DN with key <code>'dn'</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string toJson(boolean $includeSystemAttributes)</code>
+							</entry>
+							<entry>Returns a JSON representation of the current node using <code>Zend_Ldap_Node::toArray()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getData(boolean $includeSystemAttributes)</code>
+							</entry>
+							<entry>Returns the node's attributes. The array contains all attributes in its internal format (no conversion).</entry>
+						</row>
+						<row>
+							<entry>
+								<code>boolean existsAttribute(string $name, boolean $emptyExists)</code>
+							</entry>
+							<entry>Checks whether a given attribute exists. If <code>$emptyExists</code> is <code>false</code> empty attributes (containing only array()) are treated as non-existent returning <code>false</code>. If <code>$emptyExists</code> is true empty attributes are treated as existent returning <code>true</code>. In this case teh method returns <code>false</code> only if the attribute name is missing in the key-collection.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>boolean attributeHasValue(string $name, mixed|array $value)</code>
+							</entry>
+							<entry>Checks if the given value(s) exist in the attribute. The method returns <code>true</code> only if all values in <code>$value</code> are present in the attribute. Comparison is done strictly (respecting the data type).</entry>
+						</row>
+						<row>
+							<entry>
+								<code>integer count()</code>
+							</entry>
+							<entry>Returns the number of attributes in the node. Implements Countable.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>mixed getAttribute(string $name, integer|null $index)</code>
+							</entry>
+							<entry>Gets a LDAP attribute. Data conversion is applied using <code>Zend_Ldap_Attribute::getAttribute()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getAttributes(boolean $includeSystemAttributes)</code>
+							</entry>
+							<entry>Gets all attributes of node. If <code>$includeSystemAttributes</code> is <code>false</code> (defaults to <code>true</code>) the system specific attributes are stripped from the array.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node setAttribute(string $name, mixed $value)</code>
+							</entry>
+							<entry>Sets a LDAP attribute. Data conversion is applied using <code>Zend_Ldap_Attribute::setAttribute()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node appendToAttribute(string $name, mixed $value)</code>
+							</entry>
+							<entry>Appends to a LDAP attribute. Data conversion is applied using <code>Zend_Ldap_Attribute::setAttribute()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array|integer getDateTimeAttribute(string $name, integer|null $index)</code>
+							</entry>
+							<entry>Gets a LDAP date/time attribute. Data conversion is applied using <code>Zend_Ldap_Attribute::getDateTimeAttribute()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node setDateTimeAttribute(string $name, integer|array $value, boolean $utc)</code>
+							</entry>
+							<entry>Sets a LDAP date/time attribute. Data conversion is applied using <code>Zend_Ldap_Attribute::setDateTimeAttribute()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node appendToDateTimeAttribute(string $name, integer|array $value, boolean $utc)</code>
+							</entry>
+							<entry>Appends to a LDAP date/time attribute. Data conversion is applied using <code>Zend_Ldap_Attribute::setDateTimeAttribute()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node setPasswordAttribute(string $password, string $hashType, string $attribName)</code>
+							</entry>
+							<entry>Sets a LDAP password on <code>$attribName</code> (defaults to <code>'userPassword'</code>) to <code>$password</code> with the hash type <code>$hashType</code> (defaults to <code>Zend_Ldap_Attribute::PASSWORD_HASH_MD5</code>).</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node deleteAttribute(string $name)</code>
+							</entry>
+							<entry>Deletes a LDAP attribute.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>void removeDuplicatesFromAttribute(string $name)</code>
+							</entry>
+							<entry>Removes duplicate values from a LDAP attribute.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>void removeFromAttribute(string $attribName, mixed|array $value)</code>
+							</entry>
+							<entry>Removes the given values from a LDAP attribute.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>boolean exists(Zend_Ldap $ldap)</code>
+							</entry>
+							<entry>Checks if the current node exists on the given LDAP server (current server is used if <code>null</code> is passed).</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node reload(Zend_Ldap $ldap)</code>
+							</entry>
+							<entry>Reloads the current node's attributes from the given LDAP server (current server is used if <code>null</code> is passed).</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node_Collection searchSubtree(string|Zend_Ldap_Filter_Abstract $filter, integer $scope, string $sort)</code>
+							</entry>
+							<entry>Searches the nodes's subtree with the given <code>$filter</code> and the given search parameters. See <code>Zend_Ldap::search()</code> for details on the parameters <code>$scope</code> and <code>$sort</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>integer countSubtree(string|Zend_Ldap_Filter_Abstract $filter, integer $scope)</code>
+							</entry>
+							<entry>Count the nodes's subtree items matching the the given <code>$filter</code> and the given search scope. See <code>Zend_Ldap::search()</code> for details on the <code>$scope</code> parameter.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>integer countChildren()</code>
+							</entry>
+							<entry>Count the nodes's children.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node_Collection searchChildren(string|Zend_Ldap_Filter_Abstract $filter, string $sort)</code>
+							</entry>
+							<entry>Searches the nodes's children matching the given <code>$filter</code>. See <code>Zend_Ldap::search()</code> for details on the <code>$sort</code> parameter.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>boolean hasChildren()</code>
+							</entry>
+							<entry>Returns whether the current node has children.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node_ChildrenIterator getChildren()</code>
+							</entry>
+							<entry>Returns all children of the current node.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node getParent(Zend_Ldap $ldap)</code>
+							</entry>
+							<entry>Returns the parent of the current node using the LDAP connection <code>$ldap</code> (uses the current LDAP connection if omitted).</entry>
+						</row>
+					</tbody>
+				</tgroup>
+			</table>
+		</sect3>
+		<sect3 id="zend.ldap.api.reference.zend-ldap-node-rootdse">
+			<title>Zend_Ldap_Node_RootDse</title>
+			<para>The following methods are available on all vendor-specific subclasses.</para>
+			<para><code>Zend_Ldap_Node_RootDse</code> includes the magic propery accessors <code>__get()</code> and <code>__isset()</code> to access the attributes by their name. They proxy to <code>Zend_Ldap_Node_RootDse::getAttribute()</code> and <code>Zend_Ldap_Node_RootDse::existsAttribute()</code> respectively. <code>__set()</code> and <code>__unset()</code> are also implemented but they throw a <code>BadMethodCallException</code> as modifications are not allowed on RootDSE nodes. Furthermore the class implements <code>ArrayAccess</code> for array-style-access to the attributes. <code>offsetSet()</code> and <code>offsetUnset()</code> also throw a <code>BadMethodCallException</code> due ro obvious reasons.</para>
+			<table id="zend.ldap.api.reference.zend-ldap-node-rootdse.table">
+				<title>Zend_Ldap_Node_RootDse API</title>
+				<tgroup cols="2">
+					<thead>
+						<row>
+							<entry>Method</entry>
+							<entry>Description</entry>
+						</row>
+					</thead>
+					<tbody>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Dn getDn()</code>
+							</entry>
+							<entry>Gets the DN of the current node as a Zend_Ldap_Dn.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string getDnString(string $caseFold)</code>
+							</entry>
+							<entry>Gets the DN of the current node as a string.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getDnArray(string $caseFold)</code>
+							</entry>
+							<entry>Gets the DN of the current node as an array.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string getRdnString(string $caseFold)</code>
+							</entry>
+							<entry>Gets the RDN of the current node as a string.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getRdnArray(string $caseFold)</code>
+							</entry>
+							<entry>Gets the RDN of the current node as an array.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getObjectClass()</code>
+							</entry>
+							<entry>Returns the objectClass of the node.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string toString()</code>
+							</entry>
+							<entry>Returns the DN of the current node - proxies to <code>Zend_Ldap_Dn::getDnString()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string __toString()</code>
+							</entry>
+							<entry>Casts to string representation - proxies to <code>Zend_Ldap_Dn::toString()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array toArray(boolean $includeSystemAttributes)</code>
+							</entry>
+							<entry>Returns an array representation of the current node. If <code>$includeSystemAttributes</code> is <code>false</code> (defaults to <code>true</code>) the system specific attributes are stripped from the array. Unlike <code>Zend_Ldap_Node_RootDse::getAttributes()</code> the resulting array contains the DN with key <code>'dn'</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string toJson(boolean $includeSystemAttributes)</code>
+							</entry>
+							<entry>Returns a JSON representation of the current node using <code>Zend_Ldap_Node_RootDse::toArray()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getData(boolean $includeSystemAttributes)</code>
+							</entry>
+							<entry>Returns the node's attributes. The array contains all attributes in its internal format (no conversion).</entry>
+						</row>
+						<row>
+							<entry>
+								<code>boolean existsAttribute(string $name, boolean $emptyExists)</code>
+							</entry>
+							<entry>Checks whether a given attribute exists. If <code>$emptyExists</code> is <code>false</code> empty attributes (containing only array()) are treated as non-existent returning <code>false</code>. If <code>$emptyExists</code> is true empty attributes are treated as existent returning <code>true</code>. In this case teh method returns <code>false</code> only if the attribute name is missing in the key-collection.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>boolean attributeHasValue(string $name, mixed|array $value)</code>
+							</entry>
+							<entry>Checks if the given value(s) exist in the attribute. The method returns <code>true</code> only if all values in <code>$value</code> are present in the attribute. Comparison is done strictly (respecting the data type).</entry>
+						</row>
+						<row>
+							<entry>
+								<code>integer count()</code>
+							</entry>
+							<entry>Returns the number of attributes in the node. Implements Countable.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>mixed getAttribute(string $name, integer|null $index)</code>
+							</entry>
+							<entry>Gets a LDAP attribute. Data conversion is applied using <code>Zend_Ldap_Attribute::getAttribute()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getAttributes(boolean $includeSystemAttributes)</code>
+							</entry>
+							<entry>Gets all attributes of node. If <code>$includeSystemAttributes</code> is <code>false</code> (defaults to <code>true</code>) the system specific attributes are stripped from the array.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array|integer getDateTimeAttribute(string $name, integer|null $index)</code>
+							</entry>
+							<entry>Gets a LDAP date/time attribute. Data conversion is applied using <code>Zend_Ldap_Attribute::getDateTimeAttribute()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node_RootDse reload(Zend_Ldap $ldap)</code>
+							</entry>
+							<entry>Reloads the current node's attributes from the given LDAP server.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>Zend_Ldap_Node_RootDse create(Zend_Ldap $ldap)</code>
+								</emphasis>
+							</entry>
+							<entry>Factory method to create the RootDSE.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getNamingContexts()</code>
+							</entry>
+							<entry>Gets the namingContexts.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string|null getSubschemaSubentry()</code>
+							</entry>
+							<entry>Gets the subschemaSubentry.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>boolean supportsVersion(string|int|array $versions)</code>
+							</entry>
+							<entry>Determines if the LDAP version is supported.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>boolean supportsSaslMechanism(string|array $mechlist)</code>
+							</entry>
+							<entry>Determines if the sasl mechanism is supported.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>integer getServerType()</code>
+							</entry>
+							<entry>Gets the server type. Returns 
+							<variablelist>
+							<varlistentry>
+					<term>
+						<code>Zend_Ldap_Node_RootDse::SERVER_TYPE_GENERIC</code>
+					</term>
+					<listitem>for unknown LDAP servers</listitem>
+				</varlistentry>
+				<varlistentry>
+					<term>
+						<code>Zend_Ldap_Node_RootDse::SERVER_TYPE_OPENLDAP</code>
+					</term>
+					<listitem>for OpenLDAP servers</listitem>
+				</varlistentry>
+				<varlistentry>
+					<term>
+						<code>Zend_Ldap_Node_RootDse::SERVER_TYPE_ACTIVEDIRECTORY</code>
+					</term>
+					<listitem>for Microsoft ActiveDirectory servers</listitem>
+				</varlistentry>
+				<varlistentry>
+					<term>
+						<code>Zend_Ldap_Node_RootDse::SERVER_TYPE_EDIRECTORY</code>
+					</term>
+					<listitem>For Novell eDirectory servers</listitem>
+				</varlistentry>
+			</variablelist>
+							</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Dn getSchemaDn()</code>
+							</entry>
+							<entry>Returns the schema DN.</entry>
+						</row>
+					</tbody>
+				</tgroup>
+			</table>
+			<sect4 id="zend.ldap.api.reference.zend-ldap-node-rootdse.openldap">
+				<title>OpenLDAP</title>
+				<para>Additionally the common methods above apply to instances of <code>Zend_Ldap_Node_RootDse_OpenLdap</code>.</para>
+				<note><para>Refer to <ulink url="http://www.zytrax.com/books/ldap/ch3/#operational">LDAP Operational Attributes and Objects</ulink> for information on the attributes of OpenLDAP RootDSE.</para></note>
+				<table id="zend.ldap.api.reference.zend-ldap-node-rootdse.openldap.table">
+					<title>Zend_Ldap_Node_RootDse_OpenLdap API</title>
+					<tgroup cols="2">
+						<thead>
+							<row>
+								<entry>Method</entry>
+								<entry>Description</entry>
+							</row>
+						</thead>
+						<tbody>
+							<row>
+								<entry>
+									<code>integer getServerType()</code>
+								</entry>
+								<entry>Gets the server type. Returns <code>Zend_Ldap_Node_RootDse::SERVER_TYPE_OPENLDAP</code>
+								</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getConfigContext()</code>
+								</entry>
+								<entry>Gets the configContext.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getMonitorContext()</code>
+								</entry>
+								<entry>Gets the monitorContext.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>boolean supportsControl(string|array $oids)</code>
+								</entry>
+								<entry>Determines if the control is supported.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>boolean supportsExtension(string|array $oids)</code>
+								</entry>
+								<entry>Determines if the extension is supported.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>boolean supportsFeature(string|array $oids)</code>
+								</entry>
+								<entry>Determines if the feature is supported.</entry>
+							</row>
+						</tbody>
+					</tgroup>
+				</table>
+			</sect4>
+			<sect4 id="zend.ldap.api.reference.zend-ldap-node-rootdse.activedirectory">
+				<title>ActiveDirectory</title>
+				<para>Additionally the common methods above apply to instances of <code>Zend_Ldap_Node_RootDse_ActiveDirectory</code>.</para>
+				<note><para>Refer to <ulink url="http://msdn.microsoft.com/en-us/library/ms684291(VS.85).aspx">RootDSE</ulink> for information on the attributes of Microsoft ActiveDirectory RootDSE.</para></note>
+				<table id="zend.ldap.api.reference.zend-ldap-node-rootdse.activedirectory.table">
+					<title>Zend_Ldap_Node_RootDse_ActiveDirectory API</title>
+					<tgroup cols="2">
+						<thead>
+							<row>
+								<entry>Method</entry>
+								<entry>Description</entry>
+							</row>
+						</thead>
+						<tbody>
+							<row>
+								<entry>
+									<code>integer getServerType()</code>
+								</entry>
+								<entry>Gets the server type. Returns <code>Zend_Ldap_Node_RootDse::SERVER_TYPE_ACTIVEDIRECTORY</code>
+								</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getConfigurationNamingContext()</code>
+								</entry>
+								<entry>Gets the configurationNamingContext.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getCurrentTime()</code>
+								</entry>
+								<entry>Gets the currentTime.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getDefaultNamingContext()</code>
+								</entry>
+								<entry>Gets the defaultNamingContext.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getDnsHostName()</code>
+								</entry>
+								<entry>Gets the dnsHostName.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getDomainControllerFunctionality()</code>
+								</entry>
+								<entry>Gets the domainControllerFunctionality.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getDomainFunctionality()</code>
+								</entry>
+								<entry>Gets the domainFunctionality.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getDsServiceName()</code>
+								</entry>
+								<entry>Gets the dsServiceName.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getForestFunctionality()</code>
+								</entry>
+								<entry>Gets the forestFunctionality.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getHighestCommittedUSN()</code>
+								</entry>
+								<entry>Gets the highestCommittedUSN.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getIsGlobalCatalogReady()</code>
+								</entry>
+								<entry>Gets the isGlobalCatalogReady.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getIsSynchronized()</code>
+								</entry>
+								<entry>Gets the isSynchronized.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getLdapServiceName()</code>
+								</entry>
+								<entry>Gets the ldapServiceName.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getRootDomainNamingContext()</code>
+								</entry>
+								<entry>Gets the rootDomainNamingContext.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getSchemaNamingContext()</code>
+								</entry>
+								<entry>Gets the schemaNamingContext.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getServerName()</code>
+								</entry>
+								<entry>Gets the serverName.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>boolean supportsCapability(string|array $oids)</code>
+								</entry>
+								<entry>Determines if the capability is supported.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>boolean supportsControl(string|array $oids)</code>
+								</entry>
+								<entry>Determines if the control is supported.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>boolean supportsPolicy(string|array $policies)</code>
+								</entry>
+								<entry>Determines if the version is supported.</entry>
+							</row>
+						</tbody>
+					</tgroup>
+				</table>
+			</sect4>
+			<sect4 id="zend.ldap.api.reference.zend-ldap-node-rootdse.edirectory">
+				<title>eDirectory</title>
+				<para>Additionally the common methods above apply to instances of <code>Zend_Ldap_Node_RootDse_eDirectory</code>.</para>
+				<note><para>Refer to <ulink url="http://www.novell.com/documentation/edir88/edir88/index.html?page=/documentation/edir88/edir88/data/ah59jqq.html">Getting Information about the LDAP Server</ulink> for information on the attributes of Novell eDirectory RootDSE.</para></note>
+				<table id="zend.ldap.api.reference.zend-ldap-node-rootdse.edirectory.table">
+					<title>Zend_Ldap_Node_RootDse_eDirectory API</title>
+					<tgroup cols="2">
+						<thead>
+							<row>
+								<entry>Method</entry>
+								<entry>Description</entry>
+							</row>
+						</thead>
+						<tbody>
+							<row>
+								<entry>
+									<code>integer getServerType()</code>
+								</entry>
+								<entry>Gets the server type. Returns <code>Zend_Ldap_Node_RootDse::SERVER_TYPE_EDIRECTORY</code>
+								</entry>
+							</row>
+							<row>
+								<entry>
+									<code>boolean supportsExtension(string|array $oids)</code>
+								</entry>
+								<entry>Determines if the extension is supported.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getVendorName()</code>
+								</entry>
+								<entry>Gets the vendorName.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getVendorVersion()</code>
+								</entry>
+								<entry>Gets the vendorVersion.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getDsaName()</code>
+								</entry>
+								<entry>Gets the dsaName.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getStatisticsErrors()</code>
+								</entry>
+								<entry>Gets the server statistics "errors".</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getStatisticsSecurityErrors()</code>
+								</entry>
+								<entry>Gets the server statistics "securityErrors".</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getStatisticsChainings()</code>
+								</entry>
+								<entry>Gets the server statistics "chainings".</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getStatisticsReferralsReturned()</code>
+								</entry>
+								<entry>Gets the server statistics "referralsReturned".</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getStatisticsExtendedOps()</code>
+								</entry>
+								<entry>Gets the server statistics "extendedOps".</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getStatisticsAbandonOps()</code>
+								</entry>
+								<entry>Gets the server statistics "abandonOps".</entry>
+							</row>
+							<row>
+								<entry>
+									<code>string|null getStatisticsWholeSubtreeSearchOps()</code>
+								</entry>
+								<entry>Gets the server statistics "wholeSubtreeSearchOps".</entry>
+							</row>
+						</tbody>
+					</tgroup>
+				</table>
+			</sect4>
+		</sect3>
+		<sect3 id="zend.ldap.api.reference.zend-ldap-node-schema">
+			<title>Zend_Ldap_Node_Schema</title>
+			<para>The following methods are available on all vendor-specific subclasses.</para>
+			<para><code>Zend_Ldap_Node_Schema</code> includes the magic propery accessors <code>__get()</code> and <code>__isset()</code> to access the attributes by their name. They proxy to <code>Zend_Ldap_Node_Schema::getAttribute()</code> and <code>Zend_Ldap_Node_Schema::existsAttribute()</code> respectively. <code>__set()</code> and <code>__unset()</code> are also implemented but they throw a <code>BadMethodCallException</code> as modifications are not allowed on RootDSE nodes. Furthermore the class implements <code>ArrayAccess</code> for array-style-access to the attributes. <code>offsetSet()</code> and <code>offsetUnset()</code> also throw a <code>BadMethodCallException</code> due ro obvious reasons.</para>
+			<table id="zend.ldap.api.reference.zend-ldap-node-schema.table">
+				<title>Zend_Ldap_Node_Schema API</title>
+				<tgroup cols="2">
+					<thead>
+						<row>
+							<entry>Method</entry>
+							<entry>Description</entry>
+						</row>
+					</thead>
+					<tbody>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Dn getDn()</code>
+							</entry>
+							<entry>Gets the DN of the current node as a Zend_Ldap_Dn.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string getDnString(string $caseFold)</code>
+							</entry>
+							<entry>Gets the DN of the current node as a string.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getDnArray(string $caseFold)</code>
+							</entry>
+							<entry>Gets the DN of the current node as an array.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string getRdnString(string $caseFold)</code>
+							</entry>
+							<entry>Gets the RDN of the current node as a string.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getRdnArray(string $caseFold)</code>
+							</entry>
+							<entry>Gets the RDN of the current node as an array.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getObjectClass()</code>
+							</entry>
+							<entry>Returns the objectClass of the node.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string toString()</code>
+							</entry>
+							<entry>Returns the DN of the current node - proxies to <code>Zend_Ldap_Dn::getDnString()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string __toString()</code>
+							</entry>
+							<entry>Casts to string representation - proxies to <code>Zend_Ldap_Dn::toString()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array toArray(boolean $includeSystemAttributes)</code>
+							</entry>
+							<entry>Returns an array representation of the current node. If <code>$includeSystemAttributes</code> is <code>false</code> (defaults to <code>true</code>) the system specific attributes are stripped from the array. Unlike <code>Zend_Ldap_Node_Schema::getAttributes()</code> the resulting array contains the DN with key <code>'dn'</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string toJson(boolean $includeSystemAttributes)</code>
+							</entry>
+							<entry>Returns a JSON representation of the current node using <code>Zend_Ldap_Node_Schema::toArray()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getData(boolean $includeSystemAttributes)</code>
+							</entry>
+							<entry>Returns the node's attributes. The array contains all attributes in its internal format (no conversion).</entry>
+						</row>
+						<row>
+							<entry>
+								<code>boolean existsAttribute(string $name, boolean $emptyExists)</code>
+							</entry>
+							<entry>Checks whether a given attribute exists. If <code>$emptyExists</code> is <code>false</code> empty attributes (containing only array()) are treated as non-existent returning <code>false</code>. If <code>$emptyExists</code> is true empty attributes are treated as existent returning <code>true</code>. In this case teh method returns <code>false</code> only if the attribute name is missing in the key-collection.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>boolean attributeHasValue(string $name, mixed|array $value)</code>
+							</entry>
+							<entry>Checks if the given value(s) exist in the attribute. The method returns <code>true</code> only if all values in <code>$value</code> are present in the attribute. Comparison is done strictly (respecting the data type).</entry>
+						</row>
+						<row>
+							<entry>
+								<code>integer count()</code>
+							</entry>
+							<entry>Returns the number of attributes in the node. Implements Countable.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>mixed getAttribute(string $name, integer|null $index)</code>
+							</entry>
+							<entry>Gets a LDAP attribute. Data conversion is applied using <code>Zend_Ldap_Attribute::getAttribute()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getAttributes(boolean $includeSystemAttributes)</code>
+							</entry>
+							<entry>Gets all attributes of node. If <code>$includeSystemAttributes</code> is <code>false</code> (defaults to <code>true</code>) the system specific attributes are stripped from the array.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array|integer getDateTimeAttribute(string $name, integer|null $index)</code>
+							</entry>
+							<entry>Gets a LDAP date/time attribute. Data conversion is applied using <code>Zend_Ldap_Attribute::getDateTimeAttribute()</code>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>Zend_Ldap_Node_Schema reload(Zend_Ldap $ldap)</code>
+							</entry>
+							<entry>Reloads the current node's attributes from the given LDAP server.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>Zend_Ldap_Node_Schema create(Zend_Ldap $ldap)</code>
+								</emphasis>
+							</entry>
+							<entry>Factory method to create the Schema node.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getAttributeTypes()</code>
+							</entry>
+							<entry>Gets the attribute types as an array of <code/>.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getObjectClasses()</code>
+							</entry>
+							<entry>Gets the object classes as an array of <code>Zend_Ldap_Node_Schema_ObjectClass_Interface</code>.</entry>
+						</row>
+					</tbody>
+				</tgroup>
+			</table>
+			<table id="zend.ldap.api.reference.zend-ldap-node-schema.attributetype-interface.table">
+				<title>Zend_Ldap_Node_Schema_AttributeType_Interface API</title>
+				<tgroup cols="2">
+					<thead>
+						<row>
+							<entry>Method</entry>
+							<entry>Description</entry>
+						</row>
+					</thead>
+					<tbody>
+						<row>
+							<entry>
+								<code>string getName()</code>
+							</entry>
+							<entry>Gets the attribute name.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string getOid()</code>
+							</entry>
+							<entry>Gets the attribute OID.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string getSyntax()</code>
+							</entry>
+							<entry>Gets the attribute syntax.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>int|null getMaxLength()</code>
+							</entry>
+							<entry>Gets the attribute maximum length.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>boolean isSingleValued()</code>
+							</entry>
+							<entry>Returns if the attribute is single-valued.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string getDescription()</code>
+							</entry>
+							<entry>Gets the attribute description</entry>
+						</row>
+					</tbody>
+				</tgroup>
+			</table>
+			<table id="zend.ldap.api.reference.zend-ldap-node-schema.objectclass-interface.table">
+				<title>Zend_Ldap_Node_Schema_ObjectClass_Interface API</title>
+				<tgroup cols="2">
+					<thead>
+						<row>
+							<entry>Method</entry>
+							<entry>Description</entry>
+						</row>
+					</thead>
+					<tbody>
+						<row>
+							<entry>
+								<code>string getName()</code>
+							</entry>
+							<entry>Returns the objectClass name.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string getOid()</code>
+							</entry>
+							<entry>Returns the objectClass OID.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getMustContain()</code>
+							</entry>
+							<entry>Returns the attributes that this objectClass must contain.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getMayContain()</code>
+							</entry>
+							<entry>Returns the attributes that this objectClass may contain.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>string getDescription()</code>
+							</entry>
+							<entry>Returns the attribute description</entry>
+						</row>
+						<row>
+							<entry>
+								<code>integer getType()</code>
+							</entry>
+							<entry>Returns the objectClass type. The method returns one of the following values:
+<variablelist>
+				<varlistentry>
+					<term>
+						<code>Zend_Ldap_Node_Schema::OBJECTCLASS_TYPE_UNKNOWN</code>
+					</term>
+					<listitem>for unknown class types</listitem>
+				</varlistentry>
+				<varlistentry>
+					<term>
+						<code>Zend_Ldap_Node_Schema::OBJECTCLASS_TYPE_STRUCTURAL</code>
+					</term>
+					<listitem>for structural classes</listitem>
+				</varlistentry>
+				<varlistentry>
+					<term>
+						<code>Zend_Ldap_Node_Schema::OBJECTCLASS_TYPE_ABSTRACT</code>
+					</term>
+					<listitem>for abstract classes</listitem>
+				</varlistentry>
+				<varlistentry>
+					<term>
+						<code>Zend_Ldap_Node_Schema::OBJECTCLASS_TYPE_AUXILIARY</code>
+					</term>
+					<listitem>for auxiliary classes</listitem>
+				</varlistentry>
+			</variablelist>							
+							
+							</entry>
+						</row>
+						<row>
+							<entry>
+								<code>array getParentClasses()</code>
+							</entry>
+							<entry>Returns the parent objectClasses of this class. This includes structural, abstract and auxiliary objectClasses.</entry>
+						</row>
+					</tbody>
+				</tgroup>
+			</table>
+				<para>Classes representing attribute types and object classes extend <code>Zend_Ldap_Node_Schema_Item</code> which provides some core methods to access arbitrary attributes on the underlying LDAP node. <code>Zend_Ldap_Node_Schema_Item</code> includes the magic propery accessors <code>__get()</code> and <code>__isset()</code> to access the attributes by their name.  Furthermore the class implements <code>ArrayAccess</code> for array-style-access to the attributes. <code>offsetSet()</code> and <code>offsetUnset()</code>  throw a <code>BadMethodCallException</code> as modifications are not allowed on schema information nodes.</para>
+				<table id="zend.ldap.api.reference.zend-ldap-node-schema.schema-item.table">
+				<title>Zend_Ldap_Node_Schema_Item API</title>
+				<tgroup cols="2">
+					<thead>
+						<row>
+							<entry>Method</entry>
+							<entry>Description</entry>
+						</row>
+					</thead>
+					<tbody>
+						<row>
+							<entry>
+								<code>array getData()</code>
+							</entry>
+							<entry>Gets all the underlying data from the schema information node.</entry>
+						</row>
+						<row>
+							<entry>
+								<code>integer count()</code>
+							</entry>
+							<entry>Returns the number of attributes in this schema information node. Implements Countable.</entry>
+						</row>
+					</tbody>
+				</tgroup>
+				</table>
+			<sect4 id="zend.ldap.api.reference.zend-ldap-node-schema.openldap">
+				<title>OpenLDAP</title>
+				<para>Additionally the common methods above apply to instances of <code>Zend_Ldap_Node_Schema_OpenLDAP</code>.</para>
+				<table id="zend.ldap.api.reference.zend-ldap-node-schema.openldap.table">
+					<title>Zend_Ldap_Node_Schema_OpenLDAP API</title>
+					<tgroup cols="2">
+						<thead>
+							<row>
+								<entry>Method</entry>
+								<entry>Description</entry>
+							</row>
+						</thead>
+						<tbody>
+							<row>
+								<entry>
+									<code>array getLdapSyntaxes()</code>
+								</entry>
+								<entry>Gets the LDAP syntaxes.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>array getMatchingRules()</code>
+								</entry>
+								<entry>Gets the matching rules.</entry>
+							</row>
+							<row>
+								<entry>
+									<code>array getMatchingRuleUse()</code>
+								</entry>
+								<entry>Gets the matching rule use.</entry>
+							</row>
+						</tbody>
+					</tgroup>
+				</table>
+				<table id="zend.ldap.api.reference.zend-ldap-node-schema.openldap.attributetype-interface.table">
+					<title>Zend_Ldap_Node_Schema_AttributeType_OpenLDAP API</title>
+					<tgroup cols="2">
+						<thead>
+							<row>
+								<entry>Method</entry>
+								<entry>Description</entry>
+							</row>
+						</thead>
+						<tbody>
+						<row>
+							<entry><code>Zend_Ldap_Node_Schema_AttributeType_OpenLdap|null getParent()</code></entry>
+							<entry>Returns the parent attribute type in the inhertitance tree if one exists.</entry>
+						</row>
+                </tbody>
+					</tgroup>
+				</table>
+				<table id="zend.ldap.api.reference.zend-ldap-node-schema.openldap.objectclass-interface.table">
+					<title>Zend_Ldap_Node_Schema_ObjectClass_OpenLDAP API</title>
+					<tgroup cols="2">
+						<thead>
+							<row>
+								<entry>Method</entry>
+								<entry>Description</entry>
+							</row>
+						</thead>
+						<tbody>
+						<row>
+								<entry><code>array getParents()</code></entry>
+								<entry>Returns the parent object classes in the inhertitance tree if one exists. The returned array is an array of <code>Zend_Ldap_Node_Schema_ObjectClass_OpenLdap</code>.</entry>
+						</row>
+                </tbody>
+					</tgroup>
+				</table>
+			</sect4>
+			<sect4 id="zend.ldap.api.reference.zend-ldap-node-schema.activedirectory">
+				<title>ActiveDirectory</title>
+				<note>
+					<title>Schema browsing on ActiveDirectory servers</title>
+					<para>Due to restrictions on Microsoft ActiveDirectory servers regarding the number of entries returned by generic search routines and due to the structure of the ActiveDirectory schema repository, schema browsing is currently <emphasis>not</emphasis> available for Microsoft ActiveDirectory servers.</para>
+				</note>
+				<para>
+					<code>Zend_Ldap_Node_Schema_ActiveDirectory</code> does not provide any additional methods.</para>
+				<table id="zend.ldap.api.reference.zend-ldap-node-schema.activedirectory.attributetype-interface.table">
+					<title>Zend_Ldap_Node_Schema_AttributeType_ActiveDirectory API</title>
+					<tgroup cols="1">
+						<tbody>
+							<row>
+								<entry>
+									<code>Zend_Ldap_Node_Schema_AttributeType_ActiveDirectory</code> does not provide any additional methods.</entry>
+							</row>
+						</tbody>
+					</tgroup>
+				</table>
+				<table id="zend.ldap.api.reference.zend-ldap-node-schema.activedirectory.objectclass-interface.table">
+					<title>Zend_Ldap_Node_Schema_ObjectClass_ActiveDirectory API</title>
+					<tgroup cols="1">
+						<tbody>
+							<row>
+								<entry>
+									<code>Zend_Ldap_Node_Schema_ObjectClass_ActiveDirectory</code> does not provide any additional methods.</entry>
+							</row>
+						</tbody>
+					</tgroup>
+				</table>
+			</sect4>
+		</sect3>
+		<sect3 id="zend.ldap.api.reference.zend-ldap-ldif-encoder">
+			<title>Zend_Ldif_Encoder</title>
+			<table id="zend.ldap.api.reference.zend-ldap-ldif-encoder.table">
+				<title>Zend_Ldif_Encoder API</title>
+				<tgroup cols="2">
+					<thead>
+						<row>
+							<entry>Method</entry>
+							<entry>Description</entry>
+						</row>
+					</thead>
+					<tbody>
+						<row>
+							<entry>
+								<emphasis>
+									<code>array decode(string $string)</code>
+								</emphasis>
+							</entry>
+							<entry>Decodes the string <code>$string</code> into an array of LDIF items.</entry>
+						</row>
+						<row>
+							<entry>
+								<emphasis>
+									<code>string encode(scalar|array|Zend_Ldap_Node $value, array $options)</code>
+								</emphasis>
+							</entry>
+							<entry>Encode <code>$value</code> into a LDIF representation. <code>$options</code> is an array that may contain the following keys:
+                    <variablelist>
+									<varlistentry>
+										<term>
+											<code>'sort'</code>
+										</term>
+										<listitem>Sort the given attributes with <code>dn</code> following <code>objectClass</code> and following all other attributes sorted alphabetically. <code>true</code> by default.</listitem>
+									</varlistentry>
+									<varlistentry>
+										<term>
+											<code>'version'</code>
+										</term>
+										<listitem>The LDIF format version. <code>1</code> by default.</listitem>
+									</varlistentry>
+									<varlistentry>
+										<term>
+											<code>'wrap'</code>
+										</term>
+										<listitem>The line-length. <code>78</code> by default to conform to the LDIF specification.</listitem>
+									</varlistentry>
+								</variablelist>
+							</entry>
+						</row>
+					</tbody>
+				</tgroup>
+			</table>
+		</sect3>
+	</sect2>
+</sect1>

+ 265 - 0
documentation/manual/en/module_specs/Zend_Ldap-Introduction.xml

@@ -0,0 +1,265 @@
+<sect1 id="zend.ldap.introduction">
+	<title>Introduction</title>
+	<para>
+		<code>Zend_Ldap</code> is a class for performing LDAP operations including but not limited to binding,
+        searching and modifying entries in an LDAP directory.
+    </para>
+	<sect2 id="zend.ldap.introduction.theory-of-operations">
+		<title>Theory of operation</title>
+		<para>
+            This component currently consists of the main <code>Zend_Ldap</code> class, that conceptually represents a binding to a
+            single LDAP server and allows for executing operations against a LDAP server such as OpenLDAP or ActiveDirectory (AD) servers. The parameters for binding may be provided explicitly or in the form of an options array. <code>Zend_Ldap_Node</code> provides an object-oriented interface for single LDAP nodes and can be used to form a basis for an active-record-like interface for a LDAP-based domain model.
+        </para>
+		<para>
+            The component provides several helper classes to perform operations on LDAP entries (<code>Zend_Ldap_Attribute</code>) such as setting and retrieving attributes (date values, passwords, boolean values, ...), to create and modifiy LDAP filter strings (<code>Zend_Ldap_Filter</code>) and to manipulate LDAP distinguished names (DN) (<code>Zend_Ldap_Dn</code>).
+        </para>
+		<para>
+            Additionally the component abstracts LDAP schema browsing for OpenLDAP and ActiveDirectoy servers <code>Zend_Ldap_Node_Schema</code> and server information retrieval for OpenLDAP-, ActiveDirectory- and Novell eDirectory servers (<code>Zend_Ldap_Node_RootDse</code>).
+        </para>
+		<para>
+            Using the <code>Zend_Ldap</code> class depends on the type of LDAP server and is best summarized with some
+            simple examples.
+        </para>
+		<para>
+            If you are using OpenLDAP, a simple example looks like the following (note that the
+            <code>bindRequiresDn</code> option is important if you are <emphasis>not</emphasis> using AD):
+<example>
+            <programlisting role="php"><![CDATA[
+$options = array(
+    'host'              => 's0.foo.net',
+    'username'          => 'CN=user1,DC=foo,DC=net',
+    'password'          => 'pass1',
+    'bindRequiresDn'    => true,
+    'accountDomainName' => 'foo.net',
+    'baseDn'            => 'OU=Sales,DC=foo,DC=net',
+);
+$ldap = new Zend_Ldap($options);
+$acctname = $ldap->getCanonicalAccountName('abaker',
+                                           Zend_Ldap::ACCTNAME_FORM_DN);
+echo "$acctname\n";
+]]></programlisting>
+</example>
+		</para>
+		<para>
+            If you are using Microsoft AD a simple example is:
+<example>
+            <programlisting role="php"><![CDATA[
+$options = array(
+    'host'                   => 'dc1.w.net',
+    'useStartTls'            => true,
+    'username'               => 'user1@w.net',
+    'password'               => 'pass1',
+    'accountDomainName'      => 'w.net',
+    'accountDomainNameShort' => 'W',
+    'baseDn'                 => 'CN=Users,DC=w,DC=net',
+);
+$ldap = new Zend_Ldap($options);
+$acctname = $ldap->getCanonicalAccountName('bcarter',
+                                           Zend_Ldap::ACCTNAME_FORM_DN);
+echo "$acctname\n";
+]]></programlisting>
+</example>
+            Note that we use the <code>getCanonicalAccountName()</code> method to retrieve the account DN here only
+            because that is what exercises the most of what little code is currently present in this class.
+        </para>
+		<sect3 id="zend.ldap.introduction.theory-of-operations.automatic-username-canonicalization">
+			<title>Automatic Username Canonicalization When Binding</title>
+			<para>
+                If <code>bind()</code> is called with a non-DN username but <code>bindRequiresDN</code> is
+                <code>true</code> and no username in DN form was supplied as an option, the bind will fail. However, if
+                a username in DN form is supplied in the options array, <code>Zend_Ldap</code> will first bind with
+                that username, retrieve the account DN for the username supplied to <code>bind()</code> and then re-
+                bind with that DN.
+            </para>
+			<para>
+                This behavior is critical to <link linkend="zend.auth.adapter.ldap">
+					<code>Zend_Auth_Adapter_Ldap</code>
+				</link>, which passes the username supplied by
+                the user directly to <code>bind()</code>.
+            </para>
+			<para>
+                The following example illustrates how the non-DN username '<code>abaker</code>' can be used with
+                <code>bind()</code>:
+<example>
+                <programlisting role="php"><![CDATA[
+$options = array(
+        'host'              => 's0.foo.net',
+        'username'          => 'CN=user1,DC=foo,DC=net',
+        'password'          => 'pass1',
+        'bindRequiresDn'    => true,
+        'accountDomainName' => 'foo.net',
+        'baseDn'            => 'OU=Sales,DC=foo,DC=net',
+);
+$ldap = new Zend_Ldap($options);
+$ldap->bind('abaker', 'moonbike55');
+$acctname = $ldap->getCanonicalAccountName('abaker',
+                                           Zend_Ldap::ACCTNAME_FORM_DN);
+echo "$acctname\n";
+]]></programlisting>
+</example>
+                The <code>bind()</code> call in this example sees that the username '<code>abaker</code>' is not in DN
+                form, finds <code>bindRequiresDn</code> is <code>true</code>, uses
+                '<code>CN=user1,DC=foo,DC=net</code>' and '<code>pass1</code>' to bind, retrieves the DN for
+                '<code>abaker</code>', unbinds and then rebinds with the newly discovered
+                '<code>CN=Alice Baker,OU=Sales,DC=foo,DC=net</code>'.
+            </para>
+		</sect3>
+		<sect3 id="zend.ldap.introduction.theory-of-operations.account-name-canonicalization">
+			<title>Account Name Canonicalization</title>
+			<para>
+                The <code>accountDomainName</code> and <code>accountDomainNameShort</code> options are used for two
+                purposes: (1) they facilitate multi-domain authentication and failover capability, and (2) they are
+                also used to canonicalize usernames. Specifically, names are canonicalized to the form specified by the
+                <code>accountCanonicalForm</code> option. This option may one of the following values:
+
+                <table id="zend.ldap.using.theory-of-operation.account-name-canonicalization.table">
+					<title>Options for <code>accountCanonicalForm</code>
+					</title>
+					<tgroup cols="3">
+						<thead>
+							<row>
+								<entry>Name</entry>
+								<entry>Value</entry>
+								<entry>Example</entry>
+							</row>
+						</thead>
+						<tbody>
+							<row>
+								<entry>
+									<code>ACCTNAME_FORM_DN</code>
+								</entry>
+								<entry>1</entry>
+								<entry>CN=Alice Baker,CN=Users,DC=example,DC=com</entry>
+							</row>
+							<row>
+								<entry>
+									<code>ACCTNAME_FORM_USERNAME</code>
+								</entry>
+								<entry>2</entry>
+								<entry>abaker</entry>
+							</row>
+							<row>
+								<entry>
+									<code>ACCTNAME_FORM_BACKSLASH</code>
+								</entry>
+								<entry>3</entry>
+								<entry>EXAMPLE\abaker</entry>
+							</row>
+							<row>
+								<entry>
+									<code>ACCTNAME_FORM_PRINCIPAL</code>
+								</entry>
+								<entry>4</entry>
+								<entry>abaker@example.com</entry>
+							</row>
+						</tbody>
+					</tgroup>
+				</table>
+			</para>
+			<para>
+                The default canonicalization depends on what account domain name options were supplied. If
+                <code>accountDomainNameShort</code> was supplied, the default <code>accountCanonicalForm</code> value
+                is <code>ACCTNAME_FORM_BACKSLASH</code>. Otherwise, if <code>accountDomainName</code> was supplied, the
+                default is <code>ACCTNAME_FORM_PRINCIPAL</code>.
+            </para>
+			<para>
+                Account name canonicalization ensures that the string used to identify an account is consistent
+                regardless of what was supplied to <code>bind()</code>. For example, if the user supplies an account
+                name of <emphasis>abaker@example.com</emphasis> or just <emphasis>abaker</emphasis> and the
+                <code>accountCanonicalForm</code> is set to 3, the resulting canonicalized name would be
+                <emphasis>EXAMPLE\abaker</emphasis>.
+            </para>
+		</sect3>
+		<sect3 id="zend.ldap.introduction.theory-of-operations.multi-domain-failover">
+			<title>Multi-domain Authentication and Failover</title>
+			<para>
+                The <code>Zend_Ldap</code> component by itself makes no attempt to authenticate with multiple servers.
+                However, <code>Zend_Ldap</code> is specifically designed to handle this scenario gracefully. The
+                required technique is to simply iterate over an array of arrays of server options and attempt to bind
+                with each server. As described above <code>bind()</code> will automatically canonicalize each name, so
+                it does not matter if the user passes <code>abaker@foo.net</code> or <code>W\bcarter</code> or
+                <code>cdavis</code> - the <code>bind()</code> method will only succeed if the credentials were
+                successfully used in the bind.
+            </para>
+			<para>
+                Consider the following example that illustrates the technique required to implement multi-domain
+                authentication and failover:
+<example>
+                <programlisting role="php"><![CDATA[
+$acctname = 'W\\user2';
+$password = 'pass2';
+
+$multiOptions = array(
+    'server1' => array(
+        'host'                   => 's0.foo.net',
+        'username'               => 'CN=user1,DC=foo,DC=net',
+        'password'               => 'pass1',
+        'bindRequiresDn'         => true,
+        'accountDomainName'      => 'foo.net',
+        'accountDomainNameShort' => 'FOO',
+        'accountCanonicalForm'   => 4, // ACCT_FORM_PRINCIPAL
+        'baseDn'                 => 'OU=Sales,DC=foo,DC=net',
+    ),
+    'server2' => array(
+        'host'                   => 'dc1.w.net',
+        'useSsl'                 => true,
+        'username'               => 'user1@w.net',
+        'password'               => 'pass1',
+        'accountDomainName'      => 'w.net',
+        'accountDomainNameShort' => 'W',
+        'accountCanonicalForm'   => 4, // ACCT_FORM_PRINCIPAL
+        'baseDn'                 => 'CN=Users,DC=w,DC=net',
+    ),
+);
+
+$ldap = new Zend_Ldap();
+
+foreach ($multiOptions as $name => $options) {
+
+    echo "Trying to bind using server options for '$name'\n";
+
+    $ldap->setOptions($options);
+    try {
+        $ldap->bind($acctname, $password);
+        $acctname = $ldap->getCanonicalAccountName($acctname);
+        echo "SUCCESS: authenticated $acctname\n";
+        return;
+    } catch (Zend_Ldap_Exception $zle) {
+        echo '  ' . $zle->getMessage() . "\n";
+        if ($zle->getCode() === Zend_Ldap_Exception::LDAP_X_DOMAIN_MISMATCH) {
+            continue;
+        }
+    }
+}
+]]></programlisting>
+</example>
+                If the bind fails for any reason, the next set of server options is tried.
+            </para>
+			<para>
+                The <code>getCanonicalAccountName</code> call gets the canonical account name that the application
+                would presumably use to associate data with such as preferences. The
+                <code>accountCanonicalForm = 4</code> in all server options ensures that the canonical form is
+                consistent regardless of which server was ultimately used.
+            </para>
+			<para>
+                The special <code>LDAP_X_DOMAIN_MISMATCH</code> exception occurs when an account name with a domain
+                component was supplied (e.g., <code>abaker@foo.net</code> or <code>FOO\abaker</code> and not just
+                <code>abaker</code>) but the domain component did not match either domain in the currently selected
+                server options. This exception indicates that the server is not an authority for the account. In this
+                case, the bind will not be performed, thereby eliminating unnecessary communication with the server.
+                Note that the <code>continue</code> instruction has no effect in this example, but in practice for
+                error handling and debugging purposes, you will probably want to check for
+                <code>LDAP_X_DOMAIN_MISMATCH</code> as well as <code>LDAP_NO_SUCH_OBJECT</code> and
+                <code>LDAP_INVALID_CREDENTIALS</code>.
+            </para>
+			<para>
+                The above code is very similar to code used within
+                <link linkend="zend.auth.adapter.ldap">
+					<code>Zend_Auth_Adapter_Ldap</code>
+				</link>. In fact, we
+                recommend that you simply use that authentication adapter for multi-domain + failover LDAP based
+                authentication (or copy the code).
+            </para>
+		</sect3>
+	</sect2>
+</sect1>

+ 119 - 0
documentation/manual/en/module_specs/Zend_Ldap-LDIF.xml

@@ -0,0 +1,119 @@
+<sect1 id="zend.ldap.ldif">
+	<title>Serializing LDAP data to and from LDIF</title>
+	<sect2 id="zend.ldap.ldif.encode">
+		<title>Serialize a LDAP entry to LDIF</title>
+		<para>
+<programlisting role="php"><![CDATA[
+$data = array(           
+	'dn'                         => 'uid=rogasawara,ou=営業部,o=Airius',           
+	'objectclass'                => array('top', 'person', 'organizationalPerson', 'inetOrgPerson'),            
+	'uid'                        => array('rogasawara'),            
+	'mail'                       => array('rogasawara@airius.co.jp'),           
+	'givenname;lang-ja'          => array('ロドニー'),           
+	'sn;lang-ja'                 => array('小笠原'),           
+	'cn;lang-ja'                 => array('小笠原 ロドニー'),           
+	'title;lang-ja'              => array('営業部 部長'),           
+	'preferredlanguage'          => array('ja'),           
+	'givenname'                  => array('ロドニー'),            
+	'sn'                         => array('小笠原'),           
+	'cn'                         => array('小笠原 ロドニー'),           
+	'title'                      => array('営業部 部長'),           
+	'givenname;lang-ja;phonetic' => array('ろどにー'),          
+	'sn;lang-ja;phonetic'        => array('おがさわら'),            
+	'cn;lang-ja;phonetic'        => array('おがさわら ろどにー'),            
+	'title;lang-ja;phonetic'     => array('えいぎょうぶ ぶちょう'),          
+	'givenname;lang-en'          => array('Rodney'),           
+	'sn;lang-en'                 => array('Ogasawara'),           
+	'cn;lang-en'                 => array('Rodney Ogasawara'),            
+	'title;lang-en'              => array('Sales, Director'),       
+);
+$ldif = Zend_Ldap_Ldif_Encoder::encode($data, array('sort' => false, 'version' => null));
+/*
+$ldif contains:
+dn:: dWlkPXJvZ2FzYXdhcmEsb3U95Za25qWt6YOoLG89QWlyaXVz
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+uid: rogasawara
+mail: rogasawara@airius.co.jp
+givenname;lang-ja:: 44Ot44OJ44OL44O8
+sn;lang-ja:: 5bCP56yg5Y6f
+cn;lang-ja:: 5bCP56yg5Y6fIOODreODieODi+ODvA==
+title;lang-ja:: 5Za25qWt6YOoIOmDqOmVtw==
+preferredlanguage: ja
+givenname:: 44Ot44OJ44OL44O8
+sn:: 5bCP56yg5Y6f
+cn:: 5bCP56yg5Y6fIOODreODieODi+ODvA==
+title:: 5Za25qWt6YOoIOmDqOmVtw==
+givenname;lang-ja;phonetic:: 44KN44Gp44Gr44O8
+sn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJ
+cn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJIOOCjeOBqeOBq+ODvA==
+title;lang-ja;phonetic:: 44GI44GE44GO44KH44GG44G2IOOBtuOBoeOCh+OBhg==
+givenname;lang-en: Rodney
+sn;lang-en: Ogasawara
+cn;lang-en: Rodney Ogasawara
+title;lang-en: Sales, Director
+*/
+]]></programlisting>	
+		</para>
+	</sect2>
+	<sect2 id="zend.ldap.ldif.encode">
+		<title>Deserialize a LDIF string into a LDAP entry</title>
+		<para>
+<programlisting role="php"><![CDATA[
+$ldif = "dn:: dWlkPXJvZ2FzYXdhcmEsb3U95Za25qWt6YOoLG89QWlyaXVz
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+uid: rogasawara
+mail: rogasawara@airius.co.jp
+givenname;lang-ja:: 44Ot44OJ44OL44O8
+sn;lang-ja:: 5bCP56yg5Y6f
+cn;lang-ja:: 5bCP56yg5Y6fIOODreODieODi+ODvA==
+title;lang-ja:: 5Za25qWt6YOoIOmDqOmVtw==
+preferredlanguage: ja
+givenname:: 44Ot44OJ44OL44O8
+sn:: 5bCP56yg5Y6f
+cn:: 5bCP56yg5Y6fIOODreODieODi+ODvA==
+title:: 5Za25qWt6YOoIOmDqOmVtw==
+givenname;lang-ja;phonetic:: 44KN44Gp44Gr44O8
+sn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJ
+cn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJIOOCjeOBqeOBq+ODvA==
+title;lang-ja;phonetic:: 44GI44GE44GO44KH44GG44G2IOOBtuOBoeOCh+OBhg==
+givenname;lang-en: Rodney
+sn;lang-en: Ogasawara
+cn;lang-en: Rodney Ogasawara
+title;lang-en: Sales, Director";
+$data = Zend_Ldap_Ldif_Encoder::decode($ldif);
+/*
+$data = array(           
+	'dn'                         => 'uid=rogasawara,ou=営業部,o=Airius',           
+	'objectclass'                => array('top', 'person', 'organizationalPerson', 'inetOrgPerson'),            
+	'uid'                        => array('rogasawara'),            
+	'mail'                       => array('rogasawara@airius.co.jp'),           
+	'givenname;lang-ja'          => array('ロドニー'),           
+	'sn;lang-ja'                 => array('小笠原'),           
+	'cn;lang-ja'                 => array('小笠原 ロドニー'),           
+	'title;lang-ja'              => array('営業部 部長'),           
+	'preferredlanguage'          => array('ja'),           
+	'givenname'                  => array('ロドニー'),            
+	'sn'                         => array('小笠原'),           
+	'cn'                         => array('小笠原 ロドニー'),           
+	'title'                      => array('営業部 部長'),           
+	'givenname;lang-ja;phonetic' => array('ろどにー'),          
+	'sn;lang-ja;phonetic'        => array('おがさわら'),            
+	'cn;lang-ja;phonetic'        => array('おがさわら ろどにー'),            
+	'title;lang-ja;phonetic'     => array('えいぎょうぶ ぶちょう'),          
+	'givenname;lang-en'          => array('Rodney'),           
+	'sn;lang-en'                 => array('Ogasawara'),           
+	'cn;lang-en'                 => array('Rodney Ogasawara'),            
+	'title;lang-en'              => array('Sales, Director'),       
+);
+*/
+]]></programlisting>	
+		</para>
+	</sect2>
+</sect1>
+

+ 53 - 0
documentation/manual/en/module_specs/Zend_Ldap-Node.xml

@@ -0,0 +1,53 @@
+<sect1 id="zend.ldap.node">
+	<title>Object oriented access to the LDAP tree using Zend_Ldap_Node</title>
+	<sect2 id="zend.ldap.node.basic">
+		<title>Basic CRUD operations</title>
+		<sect3 id="zend.ldap.node.basic.retrieve">
+			<title>Retrieving data from the LDAP</title>
+			<sect4 id="zend.ldap.node.basic.retrieve.dn">
+				<title>Getting a node by its DN</title>
+				<para/>
+			</sect4>
+			<sect4 id="zend.ldap.node.basic.retrieve.search">
+				<title>Searching a node's subtree</title>
+				<para/>
+			</sect4>
+		</sect3>
+		<sect3 id="zend.ldap.node.basic.add">
+			<title>Adding a new node to the LDAP</title>
+			<para/>
+		</sect3>
+		<sect3 id="zend.ldap.node.basic.delete">
+			<title>Deleting a node from the LDAP</title>
+			<para/>
+		</sect3>
+		<sect3 id="zend.ldap.node.basic.update">
+			<title>Updating a node on the LDAP</title>
+			<para/>
+		</sect3>
+	</sect2>
+	<sect2 id="zend.ldap.node.extended">
+		<title>Extended operations</title>
+		<sect3 id="zend.ldap.node.extended.copy-and-move">
+			<title>Copy and move nodes in the LDAP</title>
+			<para/>
+		</sect3>
+	</sect2>
+	<sect2 id="zend.ldap.node.traversal">
+		<title>Tree traversal</title>
+		
+			<example>
+			<title>Traverse LDAP tree recursively</title>
+<programlisting role="php"><![CDATA[
+$options = array(/* ... */); 
+$ldap = new Zend_Ldap($options); 
+$ldap->bind(); 
+$ri = new RecursiveIteratorIterator($ldap->getBaseNode(), RecursiveIteratorIterator::SELF_FIRST); 
+foreach ($ri as $rdn => $n) { 
+    var_dump($n); 
+} 
+]]></programlisting>			
+			</example>
+
+	</sect2>
+</sect1>

+ 50 - 0
documentation/manual/en/module_specs/Zend_Ldap-Server.xml

@@ -0,0 +1,50 @@
+<sect1 id="zend.ldap.server">
+	<title>Getting information from the LDAP server</title>
+	<sect2 id="zend.ldap.server.rootdse">
+		<title>RootDSE</title>
+		<para>See the following documents for more information on the attributes contained within the RootDSE for a given LDAP server.</para>
+		<itemizedlist> 
+  <listitem> 
+    <para><ulink url="http://www.zytrax.com/books/ldap/ch3/#operational">OpenLDAP</ulink></para> 
+  </listitem> 
+  <listitem> 
+    <para><ulink url="http://msdn.microsoft.com/en-us/library/ms684291(VS.85).aspx">Microsoft ActiveDirectory</ulink></para> 
+  </listitem> 
+  <listitem> 
+    <para><ulink url="http://www.novell.com/documentation/edir88/edir88/index.html?page=/documentation/edir88/edir88/data/ah59jqq.html">Novell eDirectory</ulink></para> 
+  </listitem> 
+</itemizedlist> 
+	<example id="zend.ldap.server.rootdse.getting">
+		<title>Getting hands on the RootDSE</title>
+	<programlisting role="php"><![CDATA[
+$options = array(/* ... */); 
+$ldap = new Zend_Ldap($options); 
+$rootdse = $ldap->getRootDse();
+$serverType = $rootdse->getServerType();
+]]></programlisting>	
+	</example>
+	</sect2>
+	<sect2 id="zend.ldap.server.schema">
+		<title>Schema Browsing</title>
+		<example id="zend.ldap.server.schema.getting">
+			<title>Getting hands on the server schema</title>
+			<programlisting role="php"><![CDATA[
+$options = array(/* ... */); 
+$ldap = new Zend_Ldap($options); 
+$schema = $ldap->getSchema();
+$classes = $schema->getObjectClasses();
+]]></programlisting>	
+		</example>
+		<sect3 id="zend.ldap.server.schema.openldap">
+			<title>OpenLDAP</title>
+			<para/>
+		</sect3>
+		<sect3 id="zend.ldap.server.schema.activedirectory">
+			<title>ActiveDirectory</title>
+			<note>
+				<title>Schema browsing on ActiveDirectory servers</title>
+				<para>Due to restrictions on Microsoft ActiveDirectory servers regarding the number of entries returned by generic search routines and due to the structure of the ActiveDirectory schema repository, schema browsing is currently <emphasis>not</emphasis> available for Microsoft ActiveDirectory servers.</para>
+				</note>
+		</sect3>
+	</sect2>
+</sect1>

+ 42 - 0
documentation/manual/en/module_specs/Zend_Ldap-Tools.xml

@@ -0,0 +1,42 @@
+<sect1 id="zend.ldap.tools">
+	<title>Tools</title>
+	<sect2 id="zend.ldap.tools.dn">
+		<title>Creation and modification of DN strings</title>
+		<para/>
+	</sect2>
+	<sect2 id="zend.ldap.tools.filter">
+		<title>Using the filter API to create search filters</title>
+		<example>
+		<title>Create simple LDAP filters</title>
+<programlisting role="php"><![CDATA[
+$f1  = Zend_Ldap_Filter::equals('name', 'value');         // (name=value) 
+$f2  = Zend_Ldap_Filter::begins('name', 'value');         // (name=value*) 
+$f3  = Zend_Ldap_Filter::ends('name', 'value');           // (name=*value) 
+$f4  = Zend_Ldap_Filter::contains('name', 'value');       // (name=*value*) 
+$f5  = Zend_Ldap_Filter::greater('name', 'value');        // (name>value) 
+$f6  = Zend_Ldap_Filter::greaterOrEqual('name', 'value'); // (name>=value) 
+$f7  = Zend_Ldap_Filter::less('name', 'value');           // (name<value) 
+$f8  = Zend_Ldap_Filter::lessOrEqual('name', 'value');    // (name<=value) 
+$f9  = Zend_Ldap_Filter::approx('name', 'value');         // (name~=value) 
+$f10 = Zend_Ldap_Filter::any('name');                     // (name=*) 
+]]></programlisting>
+		</example>
+		<example>
+		<title>Create more complex LDAP filters</title>
+<programlisting role="php"><![CDATA[
+$f1 = Zend_Ldap_Filter::ends('name', 'value')->negate(); // (!(name=*value)) 
+ 
+$f2 = Zend_Ldap_Filter::equals('name', 'value'); 
+$f3 = Zend_Ldap_Filter::begins('name', 'value'); 
+$f4 = Zend_Ldap_Filter::ends('name', 'value'); 
+$f5 = Zend_Ldap_Filter::andFilter($f2, $f3, $f4);        // (&(name=value)(name=value*)(name=*value)) 
+$f6 = Zend_Ldap_Filter::orFilter($f2, $f3, $f4);         // (|(name=value)(name=value*)(name=*value)) 
+]]></programlisting>
+		
+		</example>
+	</sect2>
+	<sect2 id="zend.ldap.tools.attribute">
+		<title>Modify LDAP entries using the Attribute API</title>
+		<para/>
+	</sect2>
+</sect1>

+ 142 - 0
documentation/manual/en/module_specs/Zend_Ldap-Usage.xml

@@ -0,0 +1,142 @@
+<sect1 id="zend.ldap.usage">
+	<title>Usage Scenarios</title>
+	<sect2 id="zend.ldap.usage.authentication">
+		<title>Authentication scenarios</title>
+		<sect3 id="zend.ldap.usage.authentication.openldap">
+			<title>OpenLDAP</title>
+			<para/>
+		</sect3>
+		<sect3 id="zend.ldap.usage.authentication.activedirectory">
+			<title>ActiveDirectory</title>
+			<para/>
+		</sect3>
+	</sect2>
+	<sect2 id="zend.ldap.usage.basic">
+		<title>Basic CRUD operations</title>
+		<sect3 id="zend.ldap.usage.basic.retrieve">
+			<title>Retrieving data from the LDAP</title>
+			<example id="zend.ldap.usage.basic.retrieve.dn">
+			<title>Getting an entry by its DN</title>
+<programlisting role="php"><![CDATA[
+$options = array(/* ... */); 
+$ldap = new Zend_Ldap($options); 
+$ldap->bind(); 
+$hm = $ldap->getEntry('cn=Hugo Müller,ou=People,dc=my,dc=local'); 
+/*
+$hm is an array of the following structure
+array(
+    'dn'          => 'cn=Hugo Müller,ou=People,dc=my,dc=local',
+    'cn'          => array('Hugo Müller'),
+    'sn'          => array('Müller'),
+    'objectclass' => array('inetOrgPerson', 'top'),
+    ...
+)
+*/ 
+]]></programlisting>	
+			</example>
+			
+			<example id="zend.ldap.usage.basic.retrieve.exists">
+				<title>Check for the existence of a given DN</title>
+<programlisting role="php"><![CDATA[
+$options = array(/* ... */); 
+$ldap = new Zend_Ldap($options); 
+$ldap->bind(); 
+$isThere = $ldap->exists('cn=Hugo Müller,ou=People,dc=my,dc=local'); 
+]]></programlisting>	
+			</example>
+			
+			<example id="zend.ldap.usage.basic.retrieve.counting-children">
+				<title>Count children of a given DN</title>
+<programlisting role="php"><![CDATA[
+$options = array(/* ... */); 
+$ldap = new Zend_Ldap($options); 
+$ldap->bind(); 
+$childrenCount = $ldap->countChildren('cn=Hugo Müller,ou=People,dc=my,dc=local'); 
+]]></programlisting>	
+		</example>
+			
+			<example id="zend.ldap.usage.basic.retrieve.search">
+				<title>Searching the LDAP tree</title>
+<programlisting role="php"><![CDATA[
+$options = array(/* ... */); 
+$ldap = new Zend_Ldap($options); 
+$ldap->bind(); 
+$result = $ldap->search('(objectclass=*)', 'ou=People,dc=my,dc=local', Zend_Ldap_Ext::SEARCH_SCOPE_ONE); 
+foreach ($result as $item) { 
+    echo $item["dn"] . ': ' . $item['cn'][0] . PHP_EOL; 
+} 
+]]></programlisting>	
+			</example>
+		</sect3>
+		<sect3 id="zend.ldap.usage.basic.add">
+			<title>Adding data to the LDAP</title>
+			<example>
+			<title>Add a new entry to the LDAP</title>
+<programlisting role="php"><![CDATA[
+$options = array(/* ... */); 
+$ldap = new Zend_Ldap($options); 
+$ldap->bind(); 
+$entry = array(); 
+Zend_Ldap_Attribute::setAttribute($entry, 'cn', 'Hans Meier'); 
+Zend_Ldap_Attribute::setAttribute($entry, 'sn', 'Meier'); 
+Zend_Ldap_Attribute::setAttribute($entry, 'objectClass', 'inetOrgPerson'); 
+$ldap->add('cn=Hans Meier,ou=People,dc=my,dc=local', $entry); 
+]]></programlisting>	
+				</example>
+		</sect3>
+		<sect3 id="zend.ldap.usage.basic.delete">
+			<title>Deleting from the LDAP</title>
+			<example>
+			<title>Delete an existing entry from the LDAP</title>
+<programlisting role="php"><![CDATA[
+$options = array(/* ... */); 
+$ldap = new Zend_Ldap($options); 
+$ldap->bind(); 
+$ldap->delete('cn=Hans Meier,ou=People,dc=my,dc=local'); 
+]]></programlisting>	
+				</example>
+		</sect3>
+		<sect3 id="zend.ldap.usage.basic.update">
+			<title>Updating the LDAP</title>
+			<example>
+			<title>Update an existing entry on the LDAP</title>
+<programlisting role="php"><![CDATA[
+$options = array(/* ... */); 
+$ldap = new Zend_Ldap($options); 
+$ldap->bind(); 
+$hm = $ldap->getEntry('cn=Hugo Müller,ou=People,dc=my,dc=local'); 
+Zend_Ldap_Attribute::setAttribute($hm, 'mail', 'mueller@my.local'); 
+Zend_Ldap_Attribute::setPassword($hm, 'newPa$$w0rd', Zend_Ldap_Attribute::PASSWORD_HASH_SHA1); 
+$ldap->update('cn=Hugo Müller,ou=People,dc=my,dc=local', $hm); 
+]]></programlisting>	
+				</example>
+		</sect3>
+	</sect2>
+	<sect2 id="zend.ldap.usage.extended">
+		<title>Extended operations</title>
+		<sect3 id="zend.ldap.usage.extended.copy-and-move">
+			<title>Copy and move entries in the LDAP</title>
+			
+				<example id="zend.ldap.usage.extended.copy-and-move.copy">
+				<title>Copy a LDAP entry recursively with all its descendants.</title>
+<programlisting role="php"><![CDATA[
+$options = array(/* ... */); 
+$ldap = new Zend_Ldap($options); 
+$ldap->bind(); 
+$ldap->copy('cn=Hugo Müller,ou=People,dc=my,dc=local', 'cn=Hans Meier,ou=People,dc=my,dc=local', true);
+]]></programlisting>	
+				</example>
+			
+				<example id="zend.ldap.usage.extended.copy-and-move.move-to-subtree">
+					<title>Move a LDAP entry recursively with all its descendants to a different subtree.</title>
+<programlisting role="php"><![CDATA[
+$options = array(/* ... */); 
+$ldap = new Zend_Ldap($options); 
+$ldap->bind(); 
+$ldap->moveToSubtree('cn=Hugo Müller,ou=People,dc=my,dc=local', 'ou=Dismissed,dc=my,dc=local', true); 
+]]></programlisting>	
+				</example>
+
+		</sect3>
+	</sect2>
+</sect1>

+ 930 - 247
library/Zend/Ldap.php

@@ -20,7 +20,6 @@
  * @version    $Id$
  */
 
-
 /**
  * @category   Zend
  * @package    Zend_Ldap
@@ -29,6 +28,9 @@
  */
 class Zend_Ldap
 {
+    const SEARCH_SCOPE_SUB  = 1;
+    const SEARCH_SCOPE_ONE  = 2;
+    const SEARCH_SCOPE_BASE = 3;
 
     const ACCTNAME_FORM_DN        = 1;
     const ACCTNAME_FORM_USERNAME  = 2;
@@ -43,6 +45,13 @@ class Zend_Ldap
     private $_connectString;
 
     /**
+     * The options used in connecting, binding, etc.
+     *
+     * @var array
+     */
+    protected $_options = null;
+
+    /**
      * The raw LDAP extension resource.
      *
      * @var resource
@@ -50,90 +59,147 @@ class Zend_Ldap
     protected $_resource = null;
 
     /**
+     * Caches the RootDSE
+     *
+     * @var Zend_Ldap_Node
+     */
+    protected $_rootDse = null;
+
+    /**
+     * Caches the schema
+     *
+     * @var Zend_Ldap_Node
+     */
+    protected $_schema = null;
+
+    /**
+     * @deprecated will be removed, use {@see Zend_Ldap_Filter_Abstract::escapeValue()}
      * @param  string $str The string to escape.
      * @return string The escaped string
      */
     public static function filterEscape($str)
     {
-        $ret = '';
-        $len = strlen($str);
-        for ($si = 0; $si < $len; $si++) {
-            $ch = $str[$si];
-            $ord = ord($ch);
-            if ($ord < 0x20 || $ord > 0x7e || strstr('*()\/', $ch)) {
-                $ch = '\\' . dechex($ord);
-            }
-            $ret .= $ch;
-        }
-        return $ret;
+        /**
+         * @see Zend_Ldap_Filter_Abstract
+         */
+        require_once 'Zend/Ldap/Filter/Abstract.php';
+        return Zend_Ldap_Filter_Abstract::escapeValue($str);
     }
 
     /**
+     * @deprecated will be removed, use {@see Zend_Ldap_Dn::checkDn()}
      * @param  string $dn   The DN to parse
      * @param  array  $keys An optional array to receive DN keys (e.g. CN, OU, DC, ...)
      * @param  array  $vals An optional array to receive DN values
-     * @return bool   True if the DN was successfully parsed or false if the string is not a valid DN.
+     * @return boolean True if the DN was successfully parsed or false if the string is
+     * not a valid DN.
      */
     public static function explodeDn($dn, array &$keys = null, array &$vals = null)
     {
-        /* This is a classic state machine parser. Each iteration of the
-         * loop processes one character. State 1 collects the key. When equals (=)
-         * is encountered the state changes to 2 where the value is collected
-         * until a comma (,) or semicolon (;) is encountered after which we switch back
-         * to state 1. If a backslash (\) is encountered, state 3 is used to collect the
-         * following character without engaging the logic of other states.
+        /**
+         * @see Zend_Ldap_Dn
          */
-        $key = null;
-        $slen = strlen($dn);
-        $state = 1;
-        $ko = $vo = 0;
-        for ($di = 0; $di <= $slen; $di++) {
-            $ch = $di == $slen ? 0 : $dn[$di];
-            switch ($state) {
-                case 1: // collect key
-                    if ($ch === '=') {
-                        $key = trim(substr($dn, $ko, $di - $ko));
-                        if ($keys !== null) {
-                            $keys[] = $key; 
-                        }
-                        $state = 2;
-                        $vo = $di + 1;
-                    } else if ($ch === ',' || $ch === ';') {
-                        return false;
-                    }
-                    break;
-                case 2: // collect value
-                    if ($ch === '\\') {
-                        $state = 3;
-                    } else if ($ch === ',' || $ch === ';' || $ch === 0) {
-                        if ($vals !== null) {
-                            $vals[] = trim(substr($dn, $vo, $di - $vo));
-                        }
-                        $state = 1;
-                        $ko = $di + 1;
-                    } else if ($ch === '=') {
-                        return false;
-                    }
-                    break;
-                case 3: // escaped
-                    $state = 2;
-                    break;
-            }
-        }
-
-        return $state === 1 && $ko > 0; 
+        require_once 'Zend/Ldap/Dn.php';
+        return Zend_Ldap_Dn::checkDn($dn, $keys, $vals);
     }
 
     /**
-     * @param  array $options Options used in connecting, binding, etc.
+     * Constructor.
+     *
+     * @param  array|Zend_Config $options Options used in connecting, binding, etc.
      * @return void
      */
-    public function __construct(array $options = array())
+    public function __construct($options = array())
     {
         $this->setOptions($options);
     }
 
     /**
+     * Destructor.
+     *
+     * @return void
+     */
+    public function __destruct()
+    {
+        $this->disconnect();
+    }
+
+    /**
+     * @return resource The raw LDAP extension resource.
+     */
+    public function getResource()
+    {
+        return $this->_resource;
+    }
+
+    /**
+     * Return the LDAP error number of the last LDAP command
+     *
+     * @return int
+     */
+    public function getLastErrorCode()
+    {
+        $ret = @ldap_get_option($this->getResource(), LDAP_OPT_ERROR_NUMBER, $err);
+        if ($ret === true) {
+            if ($err <= -1 && $err >= -17) {
+                /**
+                 * @see Zend_Ldap_Exception
+                 */
+                require_once 'Zend/Ldap/Exception.php';
+                /* For some reason draft-ietf-ldapext-ldap-c-api-xx.txt error
+                 * codes in OpenLDAP are negative values from -1 to -17.
+                 */
+                $err = Zend_Ldap_Exception::LDAP_SERVER_DOWN + (-$err - 1);
+            }
+            return $err;
+        }
+        return 0;
+    }
+
+    /**
+     * Return the LDAP error message of the last LDAP command
+     *
+     * @param  int $errorCode
+     * @param  array $errorMessages
+     * @return string
+     */
+    public function getLastError(&$errorCode = null, array &$errorMessages = null)
+    {
+        $errorCode = $this->getLastErrorCode();
+        $errorMessages = array();
+
+        /* The various error retrieval functions can return
+         * different things so we just try to collect what we
+         * can and eliminate dupes.
+         */
+        $estr1 = @ldap_error($this->getResource());
+        if ($errorCode !== 0 && $estr1 === 'Success') {
+            $estr1 = @ldap_err2str($errorCode);
+        }
+        if (!empty($estr1)) {
+            $errorMessages[] = $estr1;
+        }
+
+        @ldap_get_option($this->getResource(), LDAP_OPT_ERROR_STRING, $estr2);
+        if (!empty($estr2) && !in_array($estr2, $errorMessages)) {
+            $errorMessages[] = $estr2;
+        }
+
+        $message = '';
+        if ($errorCode > 0) {
+            $message = '0x' . dechex($errorCode) . ' ';
+        } else {
+            $message = '';
+        }
+        if (count($errorMessages) > 0) {
+            $message .= '(' . implode('; ', $errorMessages) . ')';
+        } else {
+            $message .= '(no error message from LDAP)';
+        }
+        return $message;
+    }
+
+    /**
      * Sets the options used in connecting, binding, etc.
      *
      * Valid option keys:
@@ -151,65 +217,69 @@ class Zend_Ldap
      *  allowEmptyPassword
      *  useStartTls
      *  optRefferals
+     *  tryUsernameSplit
      *
-     * @param  array $options Options used in connecting, binding, etc.
+     * @param  array|Zend_Config $options Options used in connecting, binding, etc.
      * @return Zend_Ldap Provides a fluent interface
      * @throws Zend_Ldap_Exception
      */
-    public function setOptions(array $options)
+    public function setOptions($options)
     {
+        if ($options instanceof Zend_Config) {
+            $options = $options->toArray();
+        }
+
         $permittedOptions = array(
-            'host'                      => null,
-            'port'                      => null,
-            'useSsl'                    => null,
-            'username'                  => null,
-            'password'                  => null,
-            'bindRequiresDn'            => null,
-            'baseDn'                    => null,
-            'accountCanonicalForm'      => null,
-            'accountDomainName'         => null,
-            'accountDomainNameShort'    => null,
-            'accountFilterFormat'       => null,
-            'allowEmptyPassword'        => null,
-            'useStartTls'               => null,
-            'optReferrals'              => null,
+            'host'                   => null,
+            'port'                   => 0,
+            'useSsl'                 => false,
+            'username'               => null,
+            'password'               => null,
+            'bindRequiresDn'         => false,
+            'baseDn'                 => null,
+            'accountCanonicalForm'   => null,
+            'accountDomainName'      => null,
+            'accountDomainNameShort' => null,
+            'accountFilterFormat'    => null,
+            'allowEmptyPassword'     => false,
+            'useStartTls'            => false,
+            'optReferrals'           => false,
+            'tryUsernameSplit'       => true,
         );
 
-        $diff = array_diff_key($options, $permittedOptions);
-        if ($diff) {
-            list($key, $val) = each($diff);
-            require_once 'Zend/Ldap/Exception.php';
-            throw new Zend_Ldap_Exception(null, "Unknown Zend_Ldap option: $key");
-        }
-
         foreach ($permittedOptions as $key => $val) {
-            if (!array_key_exists($key, $options)) {
-                $options[$key] = null;
-            } else {
+            if (array_key_exists($key, $options)) {
+                $val = $options[$key];
+                unset($options[$key]);
                 /* Enforce typing. This eliminates issues like Zend_Config_Ini
                  * returning '1' as a string (ZF-3163).
                  */
                 switch ($key) {
                     case 'port':
                     case 'accountCanonicalForm':
-                        $options[$key] = (int)$options[$key];
+                        $permittedOptions[$key] = (int)$val;
                         break;
                     case 'useSsl':
                     case 'bindRequiresDn':
                     case 'allowEmptyPassword':
                     case 'useStartTls':
                     case 'optReferrals':
-                        $val = $options[$key];
-                        $options[$key] = $val === true ||
-                                $val === '1' ||
-                                strcasecmp($val, 'true') == 0;
+                    case 'tryUsernameSplit':
+                        $permittedOptions[$key] = ($val === true ||
+                                $val === '1' || strcasecmp($val, 'true') == 0);
+                        break;
+                    default:
+                        $permittedOptions[$key] = trim($val);
                         break;
                 }
             }
         }
-
-        $this->_options = $options;
-
+        if (count($options) > 0) {
+            $key = key($options);
+            require_once 'Zend/Ldap/Exception.php';
+            throw new Zend_Ldap_Exception(null, "Unknown Zend_Ldap option: $key");
+        }
+        $this->_options = $permittedOptions;
         return $this;
     }
 
@@ -222,17 +292,6 @@ class Zend_Ldap
     }
 
     /**
-     * @return resource The raw LDAP extension resource.
-     */
-    public function getResource()
-    {
-        /**
-         * @todo by reference?
-         */
-        return $this->_resource;
-    }
-
-    /**
      * @return string The hostname of the LDAP server being used to authenticate accounts
      */
     protected function _getHost()
@@ -245,9 +304,15 @@ class Zend_Ldap
      */
     protected function _getPort()
     {
-        if ($this->_options['port'])
-            return $this->_options['port'];
-        return 0;
+        return $this->_options['port'];
+    }
+
+    /**
+     * @return boolean The default SSL / TLS encrypted transport control
+     */
+    protected function _getUseSsl()
+    {
+        return $this->_options['useSsl'];
     }
 
     /**
@@ -267,23 +332,26 @@ class Zend_Ldap
     }
 
     /**
-     * @return boolean The default SSL / TLS encrypted transport control
+     * @return boolean Bind requires DN
      */
-    protected function _getUseSsl()
+    protected function _getBindRequiresDn()
     {
-        return $this->_options['useSsl'];
+        return $this->_options['bindRequiresDn'];
     }
 
     /**
-     * @return string The default base DN under which objects of interest are located
+     * Gets the base DN under which objects of interest are located
+     *
+     * @return string
      */
-    protected function _getBaseDn()
+    public function getBaseDn()
     {
         return $this->_options['baseDn'];
     }
 
     /**
-     * @return string Either ACCTNAME_FORM_BACKSLASH, ACCTNAME_FORM_PRINCIPAL or ACCTNAME_FORM_USERNAME indicating the form usernames should be canonicalized to.
+     * @return string Either ACCTNAME_FORM_BACKSLASH, ACCTNAME_FORM_PRINCIPAL or
+     * ACCTNAME_FORM_USERNAME indicating the form usernames should be canonicalized to.
      */
     protected function _getAccountCanonicalForm()
     {
@@ -295,8 +363,8 @@ class Zend_Ldap
 
         $accountCanonicalForm = $this->_options['accountCanonicalForm'];
         if (!$accountCanonicalForm) {
-            $accountDomainName = $this->_options['accountDomainName'];
-            $accountDomainNameShort = $this->_options['accountDomainNameShort'];
+            $accountDomainName = $this->_getAccountDomainName();
+            $accountDomainNameShort = $this->_getAccountDomainNameShort();
             if ($accountDomainNameShort) {
                 $accountCanonicalForm = Zend_Ldap::ACCTNAME_FORM_BACKSLASH;
             } else if ($accountDomainName) {
@@ -309,9 +377,25 @@ class Zend_Ldap
         return $accountCanonicalForm;
     }
 
+    /**
+     * @return string The account domain name
+     */
+    protected function _getAccountDomainName()
+    {
+        return $this->_options['accountDomainName'];
+    }
 
     /**
-     * @return string A format string for building an LDAP search filter to match an account
+     * @return string The short account domain name
+     */
+    protected function _getAccountDomainNameShort()
+    {
+        return $this->_options['accountDomainNameShort'];
+    }
+
+    /**
+     * @return string A format string for building an LDAP search filter to match
+     * an account
      */
     protected function _getAccountFilterFormat()
     {
@@ -319,20 +403,57 @@ class Zend_Ldap
     }
 
     /**
+     * @return boolean Allow empty passwords
+     */
+    protected function _getAllowEmptyPassword()
+    {
+        return $this->_options['allowEmptyPassword'];
+    }
+
+    /**
+     * @return boolean The default SSL / TLS encrypted transport control
+     */
+    protected function _getUseStartTls()
+    {
+        return $this->_options['useStartTls'];
+    }
+
+    /**
+     * @return boolean Opt. Referrals
+     */
+    protected function _getOptReferrals()
+    {
+        return $this->_options['optReferrals'];
+    }
+
+    /**
+     * @return boolean Try splitting the username into username and domain
+     */
+    protected function _getTryUsernameSplit()
+    {
+        return $this->_options['tryUsernameSplit'];
+    }
+
+    /**
      * @return string The LDAP search filter for matching directory accounts
      */
     protected function _getAccountFilter($acctname)
     {
+        /**
+         * @see Zend_Ldap_Filter_Abstract
+         */
+        require_once 'Zend/Ldap/Filter/Abstract.php';
         $this->_splitName($acctname, $dname, $aname);
         $accountFilterFormat = $this->_getAccountFilterFormat();
-        $aname = Zend_Ldap::filterEscape($aname);
-        if ($accountFilterFormat)
+        $aname = Zend_Ldap_Filter_Abstract::escapeValue($aname);
+        if ($accountFilterFormat) {
             return sprintf($accountFilterFormat, $aname);
-        if (!$this->_options['bindRequiresDn']) {
+        }
+        if (!$this->_getBindRequiresDn()) {
             // is there a better way to detect this?
-            return "(&(objectClass=user)(sAMAccountName=$aname))";
+            return sprintf("(&(objectClass=user)(sAMAccountName=%s))", $aname);
         }
-        return "(&(objectClass=posixAccount)(uid=$aname))";
+        return sprintf("(&(objectClass=posixAccount)(uid=%s))", $aname);
     }
 
     /**
@@ -342,9 +463,13 @@ class Zend_Ldap
      */
     protected function _splitName($name, &$dname, &$aname)
     {
-        $dname = NULL;
+        $dname = null;
         $aname = $name;
 
+        if (!$this->_getTryUsernameSplit()) {
+            return;
+        }
+
         $pos = strpos($name, '@');
         if ($pos) {
             $dname = substr($name, $pos + 1);
@@ -365,8 +490,11 @@ class Zend_Ldap
      */
     protected function _getAccountDn($acctname)
     {
-        if (Zend_Ldap::explodeDn($acctname))
-            return $acctname;
+        /**
+         * @see Zend_Ldap_Dn
+         */
+        require_once 'Zend/Ldap/Dn.php';
+        if (Zend_Ldap_Dn::checkDn($acctname)) return $acctname;
         $acctname = $this->getCanonicalAccountName($acctname, Zend_Ldap::ACCTNAME_FORM_USERNAME);
         $acct = $this->_getAccount($acctname, array('dn'));
         return $acct['dn'];
@@ -374,20 +502,24 @@ class Zend_Ldap
 
     /**
      * @param string $dname The domain name to check
-     * @return bool
+     * @return boolean
      */
     protected function _isPossibleAuthority($dname)
     {
-        if ($dname === null)
-            return true;
-        $accountDomainName = $this->_options['accountDomainName'];
-        $accountDomainNameShort = $this->_options['accountDomainNameShort'];
-        if ($accountDomainName === null && $accountDomainNameShort === null)
-            return true;
-        if (strcasecmp($dname, $accountDomainName) == 0)
-            return true;
-        if (strcasecmp($dname, $accountDomainNameShort) == 0)
-            return true;
+        if ($dname === null) {
+            return true;
+        }
+        $accountDomainName = $this->_getAccountDomainName();
+        $accountDomainNameShort = $this->_getAccountDomainNameShort();
+        if ($accountDomainName === null && $accountDomainNameShort === null) {
+            return true;
+        }
+        if (strcasecmp($dname, $accountDomainName) == 0) {
+            return true;
+        }
+        if (strcasecmp($dname, $accountDomainNameShort) == 0) {
+            return true;
+        }
         return false;
     }
 
@@ -407,12 +539,13 @@ class Zend_Ldap
              */
             require_once 'Zend/Ldap/Exception.php';
             throw new Zend_Ldap_Exception(null,
-                    "Binding domain is not an authority for user: $acctname",
-                    Zend_Ldap_Exception::LDAP_X_DOMAIN_MISMATCH);
+                "Binding domain is not an authority for user: $acctname",
+                Zend_Ldap_Exception::LDAP_X_DOMAIN_MISMATCH);
         }
 
-        if ($form === Zend_Ldap::ACCTNAME_FORM_DN)
+        if ($form === Zend_Ldap::ACCTNAME_FORM_DN) {
             return $this->_getAccountDn($acctname);
+        }
 
         if (!$uname) {
             /**
@@ -424,14 +557,15 @@ class Zend_Ldap
 
         $uname = strtolower($uname);
 
-        if ($form === 0)
+        if ($form === 0) {
             $form = $this->_getAccountCanonicalForm();
+        }
 
         switch ($form) {
             case Zend_Ldap::ACCTNAME_FORM_USERNAME:
                 return $uname;
             case Zend_Ldap::ACCTNAME_FORM_BACKSLASH:
-                $accountDomainNameShort = $this->_options['accountDomainNameShort'];
+                $accountDomainNameShort = $this->_getAccountDomainNameShort();
                 if (!$accountDomainNameShort) {
                     /**
                      * @see Zend_Ldap_Exception
@@ -441,7 +575,7 @@ class Zend_Ldap
                 }
                 return "$accountDomainNameShort\\$uname";
             case Zend_Ldap::ACCTNAME_FORM_PRINCIPAL:
-                $accountDomainName = $this->_options['accountDomainName'];
+                $accountDomainName = $this->_getAccountDomainName();
                 if (!$accountDomainName) {
                     /**
                      * @see Zend_Ldap_Exception
@@ -466,7 +600,7 @@ class Zend_Ldap
      */
     private function _getAccount($acctname, array $attrs = null)
     {
-        $baseDn = $this->_getBaseDn();
+        $baseDn = $this->getBaseDn();
         if (!$baseDn) {
             /**
              * @see Zend_Ldap_Exception
@@ -484,72 +618,37 @@ class Zend_Ldap
             throw new Zend_Ldap_Exception(null, 'Invalid account filter');
         }
 
-        if (!is_resource($this->_resource))
+        if (!is_resource($this->getResource())) {
             $this->bind();
-
-        $resource = $this->_resource;
-        $str = $accountFilter;
-        $code = 0;
-
-        /**
-         * @todo break out search operation into simple function (private for now)
-         */
-
-        if (!extension_loaded('ldap')) {
-            /**
-             * @see Zend_Ldap_Exception
-             */
-            require_once 'Zend/Ldap/Exception.php';
-            throw new Zend_Ldap_Exception(null, 'LDAP extension not loaded');
-        }
-
-        $result = @ldap_search($resource,
-                        $baseDn,
-                        $accountFilter,
-                        $attrs);
-        if (is_resource($result) === true) {
-            $count = @ldap_count_entries($resource, $result);
-            if ($count == 1) {
-                $entry = @ldap_first_entry($resource, $result);
-                if ($entry) {
-                    $acct = array('dn' => @ldap_get_dn($resource, $entry));
-                    $name = @ldap_first_attribute($resource, $entry, $berptr);
-                    while ($name) {
-                        $data = @ldap_get_values_len($resource, $entry, $name);
-                        $acct[$name] = $data;
-                        $name = @ldap_next_attribute($resource, $entry, $berptr);
-                    }
-                    @ldap_free_result($result);
-                    return $acct;
-                }
-            } else if ($count == 0) {
-                /**
-                 * @see Zend_Ldap_Exception
-                 */
-                require_once 'Zend/Ldap/Exception.php';
-                $code = Zend_Ldap_Exception::LDAP_NO_SUCH_OBJECT;
-            } else {
-
-                /**
-                 * @todo limit search to 1 record and remove some of this logic?
-                 */
-
-                $resource = null;
-                $str = "$accountFilter: Unexpected result count: $count";
-                /**
-                 * @see Zend_Ldap_Exception
-                 */
-                require_once 'Zend/Ldap/Exception.php';
-                $code = Zend_Ldap_Exception::LDAP_OPERATIONS_ERROR;
-            }
-            @ldap_free_result($result);
         }
 
+        $accounts = $this->search($accountFilter, $baseDn, self::SEARCH_SCOPE_SUB, $attrs);
+        $count = $accounts->count();
+        if ($count === 1) {
+            $acct = $accounts->getFirst();
+            $accounts->close();
+            return $acct;
+        } else if ($count === 0) {
+            /**
+             * @see Zend_Ldap_Exception
+             */
+            require_once 'Zend/Ldap/Exception.php';
+            $code = Zend_Ldap_Exception::LDAP_NO_SUCH_OBJECT;
+            $str = "No object found for: $accountFilter";
+        } else {
+            /**
+             * @see Zend_Ldap_Exception
+             */
+            require_once 'Zend/Ldap/Exception.php';
+            $code = Zend_Ldap_Exception::LDAP_OPERATIONS_ERROR;
+            $str = "Unexpected result count ($count) for: $accountFilter";
+        }
+        $accounts->close();
         /**
          * @see Zend_Ldap_Exception
          */
         require_once 'Zend/Ldap/Exception.php';
-        throw new Zend_Ldap_Exception($resource, $str, $code);
+        throw new Zend_Ldap_Exception($this, $str, $code);
     }
 
     /**
@@ -557,15 +656,16 @@ class Zend_Ldap
      */
     public function disconnect()
     {
-        if (is_resource($this->_resource)) {
+        if (is_resource($this->getResource())) {
             if (!extension_loaded('ldap')) {
                 /**
                  * @see Zend_Ldap_Exception
                  */
                 require_once 'Zend/Ldap/Exception.php';
-                throw new Zend_Ldap_Exception(null, 'LDAP extension not loaded');
+                throw new Zend_Ldap_Exception(null, 'LDAP extension not loaded',
+                    Zend_Ldap_Exception::LDAP_X_EXTENSION_NOT_LOADED);
             }
-            @ldap_unbind($this->_resource);
+            @ldap_unbind($this->getResource());
         }
         $this->_resource = null;
         return $this;
@@ -574,17 +674,31 @@ class Zend_Ldap
     /**
      * @param string $host The hostname of the LDAP server to connect to
      * @param int $port The port number of the LDAP server to connect to
+     * @param boolean $useSsl Use SSL
+     * @param boolean $useStartTls Use STARTTLS
      * @return Zend_Ldap Provides a fluent interface
      * @throws Zend_Ldap_Exception
      */
-    public function connect($host = null, $port = 0, $useSsl = false)
+    public function connect($host = null, $port = null, $useSsl = null, $useStartTls = null)
     {
-        if ($host === null)
+        if ($host === null) {
             $host = $this->_getHost();
-        if ($port === 0)
+        }
+        if ($port === null) {
             $port = $this->_getPort();
-        if ($useSsl === false)
+        } else {
+            $port = (int)$port;
+        }
+        if ($useSsl === null) {
             $useSsl = $this->_getUseSsl();
+        } else {
+            $useSsl = (bool)$useSsl;
+        }
+        if ($useStartTls === null) {
+            $useStartTls = $this->_getUseStartTls();
+        } else {
+            $useStartTls = (bool)$useStartTls;
+        }
 
         if (!$host) {
             /**
@@ -600,7 +714,7 @@ class Zend_Ldap
          * if you really care about the server's cert you can put a cert on the
          * web server.
          */
-        $url = $useSsl ? "ldaps://$host" : "ldap://$host";
+        $url = ($useSsl) ? "ldaps://$host" : "ldap://$host";
         if ($port) {
             $url .= ":$port";
         }
@@ -618,25 +732,22 @@ class Zend_Ldap
              * @see Zend_Ldap_Exception
              */
             require_once 'Zend/Ldap/Exception.php';
-            throw new Zend_Ldap_Exception(null, 'LDAP extension not loaded');
+            throw new Zend_Ldap_Exception(null, 'LDAP extension not loaded',
+                Zend_Ldap_Exception::LDAP_X_EXTENSION_NOT_LOADED);
         }
 
         /* Only OpenLDAP 2.2 + supports URLs so if SSL is not requested, just
          * use the old form.
          */
-        $resource = $useSsl ? @ldap_connect($url) : @ldap_connect($host, $port);
+        $resource = ($useSsl) ? @ldap_connect($url) : @ldap_connect($host, $port);
 
         if (is_resource($resource) === true) {
-
             $this->_resource = $resource;
 
-            $optReferrals = $this->_options['optReferrals'] ? 1 : 0;
-
+            $optReferrals = ($this->_getOptReferrals()) ? 1 : 0;
             if (@ldap_set_option($resource, LDAP_OPT_PROTOCOL_VERSION, 3) &&
                         @ldap_set_option($resource, LDAP_OPT_REFERRALS, $optReferrals)) {
-                if ($useSsl ||
-                            $this->_options['useStartTls'] !== true ||
-                            @ldap_start_tls($resource)) {
+                if ($useSsl || !$useStartTls || @ldap_start_tls($resource)) {
                     return $this;
                 }
             }
@@ -645,8 +756,7 @@ class Zend_Ldap
              * @see Zend_Ldap_Exception
              */
             require_once 'Zend/Ldap/Exception.php';
-
-            $zle = new Zend_Ldap_Exception($resource, "$host:$port");
+            $zle = new Zend_Ldap_Exception($this, "$host:$port");
             $this->disconnect();
             throw $zle;
         }
@@ -654,7 +764,7 @@ class Zend_Ldap
          * @see Zend_Ldap_Exception
          */
         require_once 'Zend/Ldap/Exception.php';
-        throw new Zend_Ldap_Exception("Failed to connect to LDAP server: $host:$port");
+        throw new Zend_Ldap_Exception(null, "Failed to connect to LDAP server: $host:$port");
     }
 
     /**
@@ -673,15 +783,19 @@ class Zend_Ldap
             $moreCreds = false;
         }
 
-        if ($username === NULL) {
+        if ($username === null) {
             /* Perform anonymous bind
              */
-            $password = NULL;
+            $password = null;
         } else {
             /* Check to make sure the username is in DN form.
              */
-            if (!Zend_Ldap::explodeDn($username)) {
-                if ($this->_options['bindRequiresDn']) {
+            /**
+             * @see Zend_Ldap_Dn
+             */
+            require_once 'Zend/Ldap/Dn.php';
+            if (!Zend_Ldap_Dn::checkDn($username)) {
+                if ($this->_getBindRequiresDn()) {
                     /* moreCreds stops an infinite loop if _getUsername does not
                      * return a DN and the bind requires it
                      */
@@ -689,21 +803,16 @@ class Zend_Ldap
                         try {
                             $username = $this->_getAccountDn($username);
                         } catch (Zend_Ldap_Exception $zle) {
-                            /**
-                             * @todo Temporary measure to deal with exception thrown for ldap extension not loaded
-                             */
-                            if (strpos($zle->getMessage(), 'LDAP extension not loaded') !== false) {
-                                throw $zle;
-                            }
-                            // end temporary measure
                             switch ($zle->getCode()) {
                                 case Zend_Ldap_Exception::LDAP_NO_SUCH_OBJECT:
                                 case Zend_Ldap_Exception::LDAP_X_DOMAIN_MISMATCH:
+                                case Zend_Ldap_Exception::LDAP_X_EXTENSION_NOT_LOADED:
                                     throw $zle;
                             }
                             throw new Zend_Ldap_Exception(null,
-                                        'Failed to retrieve DN for account: ' . $zle->getMessage(),
-                                        Zend_Ldap_Exception::LDAP_OPERATIONS_ERROR);
+                                'Failed to retrieve DN for account: ' . $username .
+                                ' [' . $zle->getMessage() . ']',
+                                Zend_Ldap_Exception::LDAP_OPERATIONS_ERROR);
                         }
                     } else {
                         /**
@@ -714,36 +823,33 @@ class Zend_Ldap
                     }
                 } else {
                     $username = $this->getCanonicalAccountName($username,
-                                Zend_Ldap::ACCTNAME_FORM_PRINCIPAL);
+                        Zend_Ldap::ACCTNAME_FORM_PRINCIPAL);
                 }
             }
         }
 
-        if (!is_resource($this->_resource))
+        if (!is_resource($this->getResource())) {
             $this->connect();
+        }
 
-        if ($username !== null &&
-                    $password === '' &&
-                    $this->_options['allowEmptyPassword'] !== true) {
+        if ($username !== null && $password === '' && $this->_getAllowEmptyPassword() !== true) {
             /**
              * @see Zend_Ldap_Exception
              */
             require_once 'Zend/Ldap/Exception.php';
-
             $zle = new Zend_Ldap_Exception(null,
-                    'Empty password not allowed - see allowEmptyPassword option.');
+                'Empty password not allowed - see allowEmptyPassword option.');
         } else {
-            if (@ldap_bind($this->_resource, $username, $password))
+            if (@ldap_bind($this->getResource(), $username, $password)) {
                 return $this;
+            }
 
-            $message = $username === null ? $this->_connectString : $username;
-
+            $message = ($username === null) ? $this->_connectString : $username;
             /**
              * @see Zend_Ldap_Exception
              */
             require_once 'Zend/Ldap/Exception.php';
-    
-            switch (Zend_Ldap_Exception::getLdapCode($this)) {
+            switch ($this->getLastErrorCode()) {
                 case Zend_Ldap_Exception::LDAP_SERVER_DOWN:
                     /* If the error is related to establishing a connection rather than binding,
                      * the connect string is more informative than the username.
@@ -751,9 +857,586 @@ class Zend_Ldap
                     $message = $this->_connectString;
             }
     
-            $zle = new Zend_Ldap_Exception($this->_resource, $message);
+            $zle = new Zend_Ldap_Exception($this, $message);
         }
         $this->disconnect();
         throw $zle;
     }
+
+    /**
+     * A global LDAP search routine for finding information.
+     *
+     * @param string|Zend_Ldap_Filter_Abstract $filter
+     * @param string|Zend_Ldap_Dn $basedn
+     * @param integer $scope
+     * @param array $attributes
+     * @param string $sort
+     * @param string $collectionClass
+     * @return Zend_Ldap_Collection
+     * @throws Zend_Ldap_Exception
+     */
+    public function search($filter, $basedn = null, $scope = self::SEARCH_SCOPE_SUB,
+        array $attributes = array(), $sort = null, $collectionClass = null)
+    {
+        if ($basedn === null) {
+            $basedn = $this->getBaseDn();
+        }
+        else if ($basedn instanceof Zend_Ldap_Dn) {
+            $basedn = $basedn->toString();
+        }
+
+        if ($filter instanceof Zend_Ldap_Filter_Abstract) {
+            $filter = $filter->toString();
+        }
+
+        switch ($scope) {
+            case self::SEARCH_SCOPE_ONE:
+                $search = @ldap_list($this->getResource(), $basedn, $filter, $attributes);
+                break;
+            case self::SEARCH_SCOPE_BASE:
+                $search = @ldap_read($this->getResource(), $basedn, $filter, $attributes);
+                break;
+            case self::SEARCH_SCOPE_SUB:
+            default:
+                $search = @ldap_search($this->getResource(), $basedn, $filter, $attributes);
+                break;
+        }
+
+        if($search === false) {
+            /**
+             * @see Zend_Ldap_Exception
+             */
+            require_once 'Zend/Ldap/Exception.php';
+            throw new Zend_Ldap_Exception($this, 'searching: ' . $filter);
+        }
+        if (!is_null($sort) && is_string($sort)) {
+            $isSorted = @ldap_sort($this->getResource(), $search, $sort);
+            if($search === false) {
+                /**
+                 * @see Zend_Ldap_Exception
+                 */
+                require_once 'Zend/Ldap/Exception.php';
+                throw new Zend_Ldap_Exception($this, 'sorting: ' . $sort);
+            }
+        }
+
+        /**
+         * Zend_Ldap_Collection_Iterator_Default
+         */
+        require_once 'Zend/Ldap/Collection/Iterator/Default.php';
+        $iterator = new Zend_Ldap_Collection_Iterator_Default($this, $search);
+        if ($collectionClass === null) {
+            /**
+             * Zend_Ldap_Collection
+             */
+            require_once 'Zend/Ldap/Collection.php';
+            return new Zend_Ldap_Collection($iterator);
+        } else {
+            /**
+             * @todo implement checks
+             */
+            return new $collectionClass($iterator);
+        }
+    }
+
+    /**
+     * Count items found by given filter.
+     *
+     * @param string|Zend_Ldap_Filter_Abstract $filter
+     * @param string|Zend_Ldap_Dn $basedn
+     * @param integer $scope
+     * @return integer
+     * @throws Zend_Ldap_Exception
+     */
+    public function count($filter, $basedn = null, $scope = self::SEARCH_SCOPE_SUB)
+    {
+        try {
+            $result = $this->search($filter, $basedn, $scope, array('dn'), null);
+        } catch (Zend_Ldap_Exception $e) {
+            if ($e->getCode() === Zend_Ldap_Exception::LDAP_NO_SUCH_OBJECT) return 0;
+            else throw $e;
+        }
+        return $result->count();
+    }
+
+    /**
+     * Count children for a given DN.
+     *
+     * @param string|Zend_Ldap_Dn $dn
+     * @return integer
+     * @throws Zend_Ldap_Exception
+     */
+    public function countChildren($dn)
+    {
+        return $this->count('(objectClass=*)', $dn, self::SEARCH_SCOPE_ONE);
+    }
+
+    /**
+     * Check if a given DN exists.
+     *
+     * @param string|Zend_Ldap_Dn $dn
+     * @return boolean
+     * @throws Zend_Ldap_Exception
+     */
+    public function exists($dn)
+    {
+        return ($this->count('(objectClass=*)', $dn, self::SEARCH_SCOPE_BASE) == 1);
+    }
+
+    /**
+     * Search LDAP registry for entries matching filter and optional attributes
+     *
+     * @param string|Zend_Ldap_Filter_Abstract $filter
+     * @param string|Zend_Ldap_Dn $basedn
+     * @param integer $scope
+     * @param array $attributes
+     * @param string $sort
+     * @return array
+     * @throws Zend_Ldap_Exception
+     */
+    public function searchEntries($filter, $basedn = null, $scope = self::SEARCH_SCOPE_SUB,
+        array $attributes = array(), $sort = null)
+    {
+        $result = $this->search($filter, $basedn, $scope, $attributes, $sort);
+        return $result->toArray();
+    }
+
+    /**
+     * Get LDAP entry by DN
+     *
+     * @param string|Zend_Ldap_Dn $dn
+     * @param array $attributes
+     * @param boolean $throwOnNotFound
+     * @return array
+     * @throws Zend_Ldap_Exception
+     */
+    public function getEntry($dn, array $attributes = array(), $throwOnNotFound = false)
+    {
+        try {
+            $result = $this->search("(objectClass=*)", $dn, self::SEARCH_SCOPE_BASE,
+                $attributes, null);
+            return $result->getFirst();
+        } catch (Zend_Ldap_Exception $e){
+            if ($throwOnNotFound !== false) throw $e;
+        }
+        return null;
+    }
+
+    /**
+     * Prepares an ldap data entry array for insert/update operation
+     *
+     * @param array $entry
+     * @return void
+     * @throws InvalidArgumentException
+     */
+    public static function prepareLdapEntryArray(array &$entry)
+    {
+        if (array_key_exists('dn', $entry)) unset($entry['dn']);
+        foreach ($entry as $key => $value) {
+            if (is_array($value)) {
+                foreach ($value as $i => $v) {
+                    if (is_null($v)) unset($value[$i]);
+                    else if (empty($v)) unset($value[$i]);
+                    else if (!is_scalar($v)) {
+                        throw new InvalidArgumentException('Only scalar values allowed in LDAP data');
+                    }
+                }
+                $entry[$key] = array_values($value);
+            } else {
+               if (is_null($value)) $entry[$key] = array();
+               else if (empty($value)) $entry[$key] = array();
+               else $entry[$key] = array($value);
+            }
+        }
+        $entry = array_change_key_case($entry, CASE_LOWER);
+    }
+
+    /**
+     * Add new information to the LDAP repository
+     *
+     * @param string|Zend_Ldap_Dn $dn
+     * @param array $entry
+     * @return Zend_Ldap *Provides a fluid interface*
+     * @throws Zend_Ldap_Exception
+     */
+    public function add($dn, array $entry)
+    {
+        if (!($dn instanceof Zend_Ldap_Dn)) {
+            $dn = Zend_Ldap_Dn::factory($dn, null);
+        }
+        self::prepareLdapEntryArray($entry);
+        foreach ($entry as $key => $value) {
+            if (is_array($value) && count($value) === 0) {
+                unset($entry[$key]);
+            }
+        }
+
+        $rdnParts = $dn->getRdn(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER);
+        foreach ($rdnParts as $key => $value) {
+            $value = Zend_Ldap_Dn::unescapeValue($value);
+            if (!array_key_exists($key, $entry) ||
+                    !in_array($value, $entry[$key]) ||
+                    count($entry[$key]) !== 1) {
+                $entry[$key] = array($value);
+            }
+        }
+        $adAttributes = array('distinguishedname', 'instancetype', 'name', 'objectcategory',
+            'objectguid', 'usnchanged', 'usncreated', 'whenchanged', 'whencreated');
+        foreach ($adAttributes as $attr) {
+            if (array_key_exists($attr, $entry)) {
+                unset($entry[$attr]);
+            }
+        }
+
+        $isAdded = @ldap_add($this->getResource(), $dn->toString(), $entry);
+        if($isAdded === false) {
+            /**
+             * @see Zend_Ldap_Exception
+             */
+            require_once 'Zend/Ldap/Exception.php';
+            throw new Zend_Ldap_Exception($this, 'adding: ' . $dn->toString());
+        }
+        return $this;
+    }
+
+    /**
+     * Update LDAP registry
+     *
+     * @param string|Zend_Ldap_Dn $dn
+     * @param array $entry
+     * @return Zend_Ldap *Provides a fluid interface*
+     * @throws Zend_Ldap_Exception
+     */
+    public function update($dn, array $entry)
+    {
+        if (!($dn instanceof Zend_Ldap_Dn)) {
+            $dn = Zend_Ldap_Dn::factory($dn, null);
+        }
+        self::prepareLdapEntryArray($entry);
+
+        $rdnParts = $dn->getRdn(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER);
+        $adAttributes = array('distinguishedname', 'instancetype', 'name', 'objectcategory',
+            'objectguid', 'usnchanged', 'usncreated', 'whenchanged', 'whencreated');
+        $stripAttributes = array_merge(array_keys($rdnParts), $adAttributes);
+        foreach ($stripAttributes as $attr) {
+            if (array_key_exists($attr, $entry)) {
+                unset($entry[$attr]);
+            }
+        }
+
+        if (count($entry) > 0) {
+            $isModified = @ldap_modify($this->getResource(), $dn->toString(), $entry);
+            if($isModified === false) {
+                /**
+                 * @see Zend_Ldap_Exception
+                 */
+                require_once 'Zend/Ldap/Exception.php';
+                throw new Zend_Ldap_Exception($this, 'updating: ' . $dn->toString());
+            }
+        }
+        return $this;
+    }
+
+    /**
+     * Save entry to LDAP registry.
+     *
+     * Internally decides if entry will be updated to added by calling
+     * {@link exists()}.
+     *
+     * @param string|Zend_Ldap_Dn $dn
+     * @param array $entry
+     * @return Zend_Ldap *Provides a fluid interface*
+     * @throws Zend_Ldap_Exception
+     */
+    public function save($dn, array $entry)
+    {
+        if ($dn instanceof Zend_Ldap_Dn) {
+            $dn = $dn->toString();
+        }
+        if ($this->exists($dn)) $this->update($dn, $entry);
+        else $this->add($dn, $entry);
+        return $this;
+    }
+
+    /**
+     * Delete an LDAP entry
+     *
+     * @param  string|Zend_Ldap_Dn $dn
+     * @param  boolean $recursively
+     * @return Zend_Ldap *Provides a fluid interface*
+     * @throws Zend_Ldap_Exception
+     */
+    public function delete($dn, $recursively = false)
+    {
+        if ($dn instanceof Zend_Ldap_Dn) {
+            $dn = $dn->toString();
+        }
+        if ($recursively === true) {
+            if ($this->countChildren($dn)>0) {
+                $children = $this->_getChildrenDns($dn);
+                foreach ($children as $c) {
+                    $this->delete($c, true);
+                }
+            }
+        }
+        $isDeleted = @ldap_delete($this->getResource(), $dn);
+        if($isDeleted === false) {
+            /**
+             * @see Zend_Ldap_Exception
+             */
+            require_once 'Zend/Ldap/Exception.php';
+            throw new Zend_Ldap_Exception($this, 'deleting: ' . $dn);
+        }
+        return $this;
+    }
+
+    /**
+     * Retrieve the immediate children DNs of the given $parentDn
+     *
+     * This method is used in recursive methods like {@see delete()}
+     * or {@see copy()}
+     *
+     * @param  string| $parentDn
+     * @return array of DNs
+     */
+    protected function _getChildrenDns($parentDn)
+    {
+        if ($parentDn instanceof Zend_Ldap_Dn) {
+            $parentDn = $parentDn->toString();
+        }
+        $children = array();
+        $search = @ldap_list($this->getResource(), $parentDn, '(objectClass=*)', array('dn'));
+        for ($entry = @ldap_first_entry($this->getResource(), $search);
+                $entry !== false;
+                $entry = @ldap_next_entry($this->getResource(), $entry)) {
+            $childDn = @ldap_get_dn($this->getResource(), $entry);
+            if ($childDn === false) {
+                /**
+                 * @see Zend_Ldap_Exception
+                 */
+                require_once 'Zend/Ldap/Exception.php';
+                throw new Zend_Ldap_Exception($this, 'getting dn');
+            }
+            $children[] = $childDn;
+        }
+        @ldap_free_result($search);
+        return $children;
+    }
+
+    /**
+     * Moves a LDAP entry from one DN to another subtree.
+     *
+     * @param string|Zend_Ldap_Dn $from
+     * @param string|Zend_Ldap_Dn $to
+     * @param boolean $recursively
+     * @param boolean $alwaysEmulate
+     * @return Zend_Ldap *Provides a fluid interface*
+     * @throws Zend_Ldap_Exception
+     */
+    public function moveToSubtree($from, $to, $recursively = false, $alwaysEmulate = false)
+    {
+        if ($from instanceof Zend_Ldap_Dn) {
+            $orgDnParts = $from->toArray();
+        } else {
+            $orgDnParts = Zend_Ldap_Dn::explodeDn($from);
+        }
+
+        if ($to instanceof Zend_Ldap_Dn) {
+            $newParentDnParts = $to->toArray();
+        } else {
+            $newParentDnParts = Zend_Ldap_Dn::explodeDn($to);
+        }
+
+        $newDnParts = array_merge(array(array_shift($orgDnParts)), $newParentDnParts);
+        $newDn = Zend_Ldap_Dn::fromArray($newDnParts);
+        return $this->rename($from, $newDn, $recursively, $alwaysEmulate);
+    }
+
+    /**
+     * Moves a LDAP entry from one DN to another DN.
+     *
+     * This is an alias for {@link rename()}
+     *
+     * @param string|Zend_Ldap_Dn $from
+     * @param string|Zend_Ldap_Dn $to
+     * @param boolean $recursively
+     * @param boolean $alwaysEmulate
+     * @return Zend_Ldap *Provides a fluid interface*
+     * @throws Zend_Ldap_Exception
+     */
+    public function move($from, $to, $recursively = false, $alwaysEmulate = false)
+    {
+        return $this->rename($from, $to, $recursively, $alwaysEmulate);
+    }
+
+    /**
+     * Renames a LDAP entry from one DN to another DN.
+     *
+     * This method implicitely moves the entry to another location within the tree.
+     *
+     * @param string|Zend_Ldap_Dn $from
+     * @param string|Zend_Ldap_Dn $to
+     * @param boolean $recursively
+     * @param boolean $alwaysEmulate
+     * @return Zend_Ldap *Provides a fluid interface*
+     * @throws Zend_Ldap_Exception
+     */
+    public function rename($from, $to, $recursively = false, $alwaysEmulate = false)
+    {
+        $emulate = (bool)$alwaysEmulate;
+        if (!function_exists('ldap_rename')) $emulate = true;
+        else if ($recursively) $emulate = true;
+
+        if ($emulate === false) {
+            if ($from instanceof Zend_Ldap_Dn) {
+                $from = $from->toString();
+            }
+
+            if ($to instanceof Zend_Ldap_Dn) {
+                $newDnParts = $to->toArray();
+            } else {
+                $newDnParts = Zend_Ldap_Dn::explodeDn($to);
+            }
+
+            $newRdn = Zend_Ldap_Dn::implodeRdn(array_shift($newDnParts));
+            $newParent = Zend_Ldap_Dn::implodeDn($newDnParts);
+            $isOK = @ldap_rename($this->getResource(), $from, $newRdn, $newParent, true);
+            if($isOK === false) {
+                /**
+                 * @see Zend_Ldap_Exception
+                 */
+                require_once 'Zend/Ldap/Exception.php';
+                throw new Zend_Ldap_Exception($this, 'renaming ' . $from . ' to ' . $to);
+            }
+            else if (!$this->exists($to)) $emulate = true;
+        }
+        if ($emulate) {
+            $this->copy($from, $to, $recursively);
+            $this->delete($from, $recursively);
+        }
+        return $this;
+    }
+
+    /**
+     * Copies a LDAP entry from one DN to another subtree.
+     *
+     * @param string|Zend_Ldap_Dn $from
+     * @param string|Zend_Ldap_Dn $to
+     * @param boolean $recursively
+     * @return Zend_Ldap *Provides a fluid interface*
+     * @throws Zend_Ldap_Exception
+     */
+    public function copyToSubtree($from, $to, $recursively = false)
+    {
+        if ($from instanceof Zend_Ldap_Dn) {
+            $orgDnParts = $from->toArray();
+        } else {
+            $orgDnParts = Zend_Ldap_Dn::explodeDn($from);
+        }
+
+        if ($to instanceof Zend_Ldap_Dn) {
+            $newParentDnParts = $to->toArray();
+        } else {
+            $newParentDnParts = Zend_Ldap_Dn::explodeDn($to);
+        }
+
+        $newDnParts = array_merge(array(array_shift($orgDnParts)), $newParentDnParts);
+        $newDn = Zend_Ldap_Dn::fromArray($newDnParts);
+        return $this->copy($from, $newDn, $recursively);
+    }
+
+    /**
+     * Copies a LDAP entry from one DN to another DN.
+     *
+     * @param string|Zend_Ldap_Dn $from
+     * @param string|Zend_Ldap_Dn $to
+     * @param boolean $recursively
+     * @return Zend_Ldap *Provides a fluid interface*
+     * @throws Zend_Ldap_Exception
+     */
+    public function copy($from, $to, $recursively = false)
+    {
+        $entry = $this->getEntry($from, array(), true);
+
+        if ($to instanceof Zend_Ldap_Dn) {
+            $toDnParts = $to->toArray();
+        } else {
+            $toDnParts = Zend_Ldap_Dn::explodeDn($to);
+        }
+        $this->add($to, $entry);
+
+        if ($recursively === true && $this->countChildren($from)>0) {
+            $children = $this->_getChildrenDns($from);
+            foreach ($children as $c) {
+                $cDnParts = Zend_Ldap_Dn::explodeDn($c);
+                $newChildParts = array_merge(array(array_shift($cDnParts)), $toDnParts);
+                $newChild = Zend_Ldap_Dn::implodeDn($newChildParts);
+                $this->copy($c, $newChild, true);
+            }
+        }
+        return $this;
+    }
+
+    /**
+     * Returns the specified DN as a Zend_Ldap_Node
+     *
+     * @param  string|Zend_Ldap_Dn $dn
+     * @return Zend_Ldap_Node
+     * @throws Zend_Ldap_Exception
+     */
+    public function getNode($dn)
+    {
+        /**
+         * Zend_Ldap_Node
+         */
+        require_once 'Zend/Ldap/Node.php';
+        return Zend_Ldap_Node::fromLdap($dn, $this);
+    }
+
+    /**
+     * Returns the base node as a Zend_Ldap_Node
+     *
+     * @return Zend_Ldap_Node
+     * @throws Zend_Ldap_Exception
+     */
+    public function getBaseNode()
+    {
+        return $this->getNode($this->getBaseDn(), $this);
+    }
+
+    /**
+     * Returns the RootDSE
+     *
+     * @return Zend_Ldap_Node_RootDse
+     * @throws Zend_Ldap_Exception
+     */
+    public function getRootDse()
+    {
+        if ($this->_rootDse === null) {
+            /**
+             * @see Zend_Ldap_Node_Schema
+             */
+            require_once 'Zend/Ldap/Node/RootDse.php';
+            $this->_rootDse = Zend_Ldap_Node_RootDse::create($this);
+        }
+        return $this->_rootDse;
+    }
+
+    /**
+     * Returns the schema
+     *
+     * @return Zend_Ldap_Node_Schema
+     * @throws Zend_Ldap_Exception
+     */
+    public function getSchema()
+    {
+        if ($this->_schema === null) {
+            /**
+             * @see Zend_Ldap_Node_Schema
+             */
+            require_once 'Zend/Ldap/Node/Schema.php';
+            $this->_schema = Zend_Ldap_Node_Schema::create($this);
+        }
+        return $this->_schema;
+    }
 }

+ 364 - 0
library/Zend/Ldap/Attribute.php

@@ -0,0 +1,364 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * Zend_Ldap_Attribute is a collection of LDAP attribute related functions.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Attribute
+{
+    const PASSWORD_HASH_MD5 = 'md5';
+    const PASSWORD_HASH_SHA = 'sha1';
+
+    /**
+     * Sets a LDAP attribute.
+     *
+     * @param  array $data
+     * @param  string $attribName
+     * @param  mixed $value
+     * @param  boolean $append
+     * @return void
+     */
+    public static function setAttribute(array &$data, $attribName, $value, $append = false)
+    {
+        $attribName = strtolower($attribName);
+        $valArray = array();
+        if (is_array($value) || ($value instanceof Traversable))
+        {
+            foreach ($value as $v)
+            {
+                $v = self::_valueToLdap($v);
+                if (!is_null($v)) $valArray[] = $v;
+            }
+        }
+        else if (!is_null($value))
+        {
+            $value = self::_valueToLdap($value);
+            if (!is_null($value)) $valArray[] = $value;
+        }
+
+        if ($append === true && isset($data[$attribName]))
+        {
+            if (is_string($data[$attribName])) $data[$attribName] = array($data[$attribName]);
+            $data[$attribName] = array_merge($data[$attribName], $valArray);
+        }
+        else
+        {
+            $data[$attribName] = $valArray;
+        }
+    }
+
+    /**
+     * Gets a LDAP attribute.
+     *
+     * @param  array $data
+     * @param  string $attribName
+     * @param  integer $index
+     * @return array|mixed
+     */
+    public static function getAttribute(array $data, $attribName, $index = null)
+    {
+        $attribName = strtolower($attribName);
+        if (is_null($index)) {
+            if (!isset($data[$attribName])) return array();
+            $retArray = array();
+            foreach ($data[$attribName] as $v)
+            {
+                $retArray[] = self::_valueFromLdap($v);
+            }
+            return $retArray;
+        } else if (is_int($index)) {
+            if (!isset($data[$attribName])) {
+                return null;
+            } else if ($index >= 0 && $index<count($data[$attribName])) {
+                return self::_valueFromLdap($data[$attribName][$index]);
+            } else {
+                return null;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Checks if the given value(s) exist in the attribute
+     *
+     * @param array $data
+     * @param string $attribName
+     * @param mixed|array $value
+     * @return boolean
+     */
+    public static function attributeHasValue(array &$data, $attribName, $value)
+    {
+        $attribName = strtolower($attribName);
+        if (!isset($data[$attribName])) return false;
+
+        if (is_scalar($value)) {
+            $value = array($value);
+        }
+
+        foreach ($value as $v) {
+            $v = self::_valueToLdap($v);
+            if (!in_array($v, $data[$attribName], true)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Removes duplicate values from a LDAP attribute
+     *
+     * @param array $data
+     * @param string $attribName
+     * @return void
+     */
+    public static function removeDuplicatesFromAttribute(array &$data, $attribName)
+    {
+        $attribName = strtolower($attribName);
+        if (!isset($data[$attribName])) return;
+        $data[$attribName] = array_values(array_unique($data[$attribName]));
+    }
+
+    /**
+     * Remove given values from a LDAP attribute
+     *
+     * @param array $data
+     * @param string $attribName
+     * @param mixed|array $value
+     * @return void
+     */
+    public static function removeFromAttribute(array &$data, $attribName, $value)
+    {
+        $attribName = strtolower($attribName);
+        if (!isset($data[$attribName])) return;
+
+        if (is_scalar($value)) {
+            $value = array($value);
+        }
+
+        $valArray = array();
+        foreach ($value as $v)
+        {
+            $v = self::_valueToLdap($v);
+            if ($v !== null) $valArray[] = $v;
+        }
+
+        $resultArray = $data[$attribName];
+        foreach ($valArray as $rv) {
+            $keys = array_keys($resultArray, $rv);
+            foreach ($keys as $k) {
+                unset($resultArray[$k]);
+            }
+        }
+        $resultArray = array_values($resultArray);
+        $data[$attribName] = $resultArray;
+    }
+
+    private static function _valueToLdap($value)
+    {
+        if (is_string($value)) return $value;
+        else if (is_int($value) || is_float($value)) return (string)$value;
+        else if (is_bool($value)) return ($value === true) ? 'TRUE' : 'FALSE';
+        else if (is_object($value) || is_array($value)) return serialize($value);
+        else if (is_resource($value) && get_resource_type($value) === 'stream')
+            return stream_get_contents($value);
+        else return null;
+    }
+
+    private static function _valueFromLdap($value)
+    {
+        $value = (string)$value;
+        if ($value === 'TRUE') return true;
+        else if ($value === 'FALSE') return false;
+        else return $value;
+    }
+
+    /**
+     * Converts a PHP data type into its LDAP representation
+     *
+     * @param mixed $value
+     * @return string|null - null if the PHP data type cannot be converted.
+     */
+    public static function convertToLdapValue($value)
+    {
+        return self::_valueToLdap($value);
+    }
+
+    /**
+     * Converts an LDAP value into its PHP data type
+     *
+     * @param string $value
+     * @return mixed
+     */
+    public static function convertFromLdapValue($value)
+    {
+        return self::_valueFromLdap($value);
+    }
+
+    /**
+     * Converts a timestamp into its LDAP date/time representation
+     *
+     * @param integer $value
+     * @param boolean $utc
+     * @return string|null - null if the value cannot be converted.
+     */
+    public static function convertToLdapDateTimeValue($value, $utc = false)
+    {
+        return self::_valueToLdapDateTime($value, $utc);
+    }
+
+    /**
+     * Converts LDAP date/time representation into a timestamp
+     *
+     * @param string $value
+     * @return integer|null - null if the value cannot be converted.
+     */
+    public static function convertFromLdapDateTimeValue($value)
+    {
+        return self::_valueFromLdapDateTime($value);
+    }
+
+    /**
+     * Sets a LDAP password.
+     *
+     * @param  array $data
+     * @param  string $password
+     * @param  string $hashType
+     * @param  string $attribName
+     * @return null
+     */
+    public static function setPassword(array &$data, $password, $hashType = self::PASSWORD_HASH_MD5,
+        $attribName = 'userPassword')
+    {
+        $hash = self::createPassword($password, $hashType);
+        self::setAttribute($data, $attribName, $hash, false);
+    }
+
+    /**
+     * Creates a LDAP password.
+     *
+     * @param  string $password
+     * @param  string $hashType
+     * @return string
+     */
+    public static function createPassword($password, $hashType = self::PASSWORD_HASH_MD5)
+    {
+        switch ($hashType) {
+            case self::PASSWORD_HASH_SHA:
+                $rawHash = sha1($password, true);
+                $method = '{SHA}';
+                break;
+            case self::PASSWORD_HASH_MD5:
+            default:
+                $rawHash = md5($password, true);
+                $method = '{MD5}';
+                break;
+        }
+        return $method . base64_encode($rawHash);
+    }
+
+    /**
+     * Sets a LDAP date/time attribute.
+     *
+     * @param  array $data
+     * @param  string $attribName
+     * @param  integer|array $value
+     * @param  boolean $utc
+     * @param  boolean $append
+     * @return null
+     */
+    public static function setDateTimeAttribute(array &$data, $attribName, $value, $utc = false,
+        $append = false)
+    {
+        $convertedValues = array();
+        if (is_array($value) || ($value instanceof Traversable))
+        {
+            foreach ($value as $v) {
+                $v = self::_valueToLdapDateTime($v, $utc);
+                if (!is_null($v)) $convertedValues[] = $v;
+            }
+        }
+        else if (!is_null($value)) {
+            $value = self::_valueToLdapDateTime($value, $utc);
+            if (!is_null($value)) $convertedValues[] = $value;
+        }
+        self::setAttribute($data, $attribName, $convertedValues, $append);
+    }
+
+    private static function _valueToLdapDateTime($value, $utc)
+    {
+        if (is_int($value)) {
+            if ($utc === true) return gmdate('YmdHis', $value) . 'Z';
+            else return date('YmdHisO', $value);
+        }
+        else return null;
+    }
+
+    /**
+     * Gets a LDAP date/time attribute.
+     *
+     * @param  array $data
+     * @param  string $attribName
+     * @param  integer $index
+     * @return array|integer
+     */
+    public static function getDateTimeAttribute(array $data, $attribName, $index = null)
+    {
+        $values = self::getAttribute($data, $attribName, $index);
+        if (is_array($values)) {
+            for ($i = 0; $i<count($values); $i++) {
+                $newVal = self::_valueFromLdapDateTime($values[$i]);
+                if (!is_null($newVal)) $values[$i] = $newVal;
+            }
+        }
+        else $values = self::_valueFromLdapDateTime($values);
+        return $values;
+    }
+
+    private static function _valueFromLdapDateTime($value)
+    {
+        $matches = array();
+        if (preg_match('/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})([+-]\d{4}|Z)$/', $value, $matches)) {
+            $year = $matches[1];
+            $month = $matches[2];
+            $day = $matches[3];
+            $hour = $matches[4];
+            $minute = $matches[5];
+            $second = $matches[6];
+            $timezone = $matches[7];
+            $date = gmmktime($hour, $minute, $second, $month, $day, $year);
+            if ($timezone !== 'Z') {
+                $tzDirection = substr($timezone, 0, 1);
+                $tzOffsetHour = substr($timezone, 1, 2);
+                $tzOffsetMinute = substr($timezone, 3, 2);
+                $tzOffset = ($tzOffsetHour*60*60) + ($tzOffsetMinute*60);
+                if ($tzDirection == '+') $date -= $tzOffset;
+                else if ($tzDirection == '-') $date += $tzOffset;
+            }
+            return $date;
+        }
+        else return null;
+    }
+}

+ 197 - 0
library/Zend/Ldap/Collection.php

@@ -0,0 +1,197 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * Zend_Ldap_Collection wraps a list of LDAP entries.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Collection implements Iterator, Countable
+{
+    /**
+     * Iterator
+     *
+     * @var Zend_Ldap_Collection_Iterator_Interface
+     */
+    protected $_iterator = null;
+
+    /**
+     * Current item number
+     *
+     * @var integer
+     */
+    protected $_currentNumber = -1;
+
+    /**
+     * Container for item caching to speed up multiple iterations
+     *
+     * @var array
+     */
+    protected $_cache = array();
+
+    /**
+     * Constructor.
+     *
+     * @param Zend_Ldap $ldap
+     * @param Zend_Ldap_Collection_Iterator_Interface $iterator
+     * @return void
+     */
+    public function __construct(Zend_Ldap_Collection_Iterator_Interface $iterator)
+    {
+        $this->_iterator = $iterator;
+    }
+
+    public function __destruct()
+    {
+        $this->close();
+    }
+
+    /**
+     * Closes the current result set
+     *
+     * @return boolean
+     */
+    public function close()
+    {
+        return $this->_iterator->close();
+    }
+
+    /**
+     * Get all entries as an array
+     *
+     * @return array
+     */
+    public function toArray()
+    {
+        $data = array();
+        foreach ($this as $item) {
+            $data[] = $item;
+        }
+        return $data;
+    }
+
+    /**
+     * Get first entry
+     *
+     * @return array
+     */
+    public function getFirst()
+    {
+        if ($this->count()>0) {
+            $this->rewind();
+            return $this->current();
+        }
+        else return null;
+    }
+
+    /**
+     * Returns the number of items in current result
+     * Implements Countable
+     *
+     * @return int
+     */
+    public function count()
+    {
+        return $this->_iterator->count();
+    }
+
+    /**
+     * Return the current result item
+     * Implements Iterator
+     *
+     * @return array
+     * @throws Zend_Ldap_Exception
+     */
+    public function current()
+    {
+        if (!array_key_exists($this->_currentNumber, $this->_cache))
+        {
+            $this->_cache[$this->_currentNumber] =
+                $this->_createEntry($this->_iterator->current());
+        }
+        return $this->_cache[$this->_currentNumber];
+    }
+
+    /**
+     * Creates the data structure for the given entry data
+     *
+     * @param array $data
+     * @return array
+     */
+    protected function _createEntry(array $data)
+    {
+        return $data;
+    }
+
+    /**
+     * Return the result item key
+     * Implements Iterator
+     *
+     * @return int
+     */
+    public function key()
+    {
+        return $this->_currentNumber;
+    }
+
+    /**
+     * Move forward to next result item
+     * Implements Iterator
+     *
+     * @throws Zend_Ldap_Exception
+     */
+    public function next()
+    {
+        $this->_iterator->next();
+        $this->_currentNumber++;
+    }
+
+    /**
+     * Rewind the Iterator to the first result item
+     * Implements Iterator
+     *
+     * @throws Zend_Ldap_Exception
+     */
+    public function rewind()
+    {
+        $this->_iterator->rewind();
+        $this->_currentNumber = 0;
+    }
+
+    /**
+     * Check if there is a current result item
+     * after calls to rewind() or next()
+     * Implements Iterator
+     *
+     * @return boolean
+     */
+    public function valid()
+    {
+        if (isset($this->_cache[$this->_currentNumber])) {
+            return true;
+        } else {
+            return $this->_iterator->valid();
+        }
+    }
+}

+ 253 - 0
library/Zend/Ldap/Collection/Iterator/Default.php

@@ -0,0 +1,253 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Ldap_Collection_Iterator_Interface
+ */
+require_once 'Zend/Ldap/Collection/Iterator/Interface.php';
+
+/**
+ * Zend_Ldap_Collection_Iterator_Default is the default collection iterator implementation
+ * using ext/ldap
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Collection_Iterator_Default implements Zend_Ldap_Collection_Iterator_Interface
+{
+    /**
+     * LDAP Connection
+     *
+     * @var Zend_Ldap
+     */
+    protected $_ldap = null;
+
+    /**
+     * Result identifier resource
+     *
+     * @var resource
+     */
+    protected $_resultId = null;
+
+    /**
+     * Current result entry identifier
+     *
+     * @var resource
+     */
+    protected $_current = null;
+
+    /**
+     * Current result entry DN
+     *
+     * @var string
+     */
+    protected $_currentDn = null;
+
+    /**
+     * Number of items in query result
+     *
+     * @var integer
+     */
+    protected $_itemCount = -1;
+
+    /**
+     * Constructor.
+     *
+     * @param Zend_Ldap $ldap
+     * @param resource $resultId
+     * @return void
+     */
+    public function __construct(Zend_Ldap $ldap, $resultId)
+    {
+        $this->_ldap = $ldap;
+        $this->_resultId = $resultId;
+        $this->_itemCount = @ldap_count_entries($ldap->getResource(), $resultId);
+        if ($this->_itemCount === false) {
+            /**
+             * @see Zend_Ldap_Exception
+             */
+            require_once 'Zend/Ldap/Exception.php';
+            throw new Zend_Ldap_Exception($this->_ldap, 'counting entries');
+        }
+    }
+
+    public function __destruct()
+    {
+        $this->close();
+    }
+
+    /**
+     * Closes the current result set
+     *
+     * @return bool
+     */
+    public function close()
+    {
+        $isClosed = false;
+        if (is_resource($this->_resultId)) {
+             $isClosed = @ldap_free_result($this->_resultId);
+             $this->_resultId = null;
+             $this->_currentDn = null;
+             $this->_current = null;
+        }
+        return $isClosed;
+    }
+
+    /**
+     * Gets the current LDAP connection.
+     *
+     * @return Zend_Ldap
+     */
+    public function getLdap()
+    {
+        return $this->_ldap;
+    }
+
+    /**
+     * Returns the number of items in current result
+     * Implements Countable
+     *
+     * @return int
+     */
+    public function count()
+    {
+        return $this->_itemCount;
+    }
+
+    /**
+     * Return the current result item
+     * Implements Iterator
+     *
+     * @return array
+     * @throws Zend_Ldap_Exception
+     */
+    public function current()
+    {
+        if (!is_resource($this->_current) || !is_string($this->_currentDn)) return null;
+
+        $entry = array('dn' => $this->_currentDn);
+        $ber_identifier = null;
+        $name = @ldap_first_attribute($this->_ldap->getResource(), $this->_current,
+            $ber_identifier);
+        while ($name)
+        {
+            $data = @ldap_get_values_len($this->_ldap->getResource(), $this->_current, $name);
+            unset($data['count']);
+            $entry[strtolower($name)] = $data;
+            $name = @ldap_next_attribute($this->_ldap->getResource(), $this->_current,
+                $ber_identifier);
+        }
+        ksort($entry, SORT_LOCALE_STRING);
+        return $entry;
+    }
+
+    /**
+     * Return the result item key
+     * Implements Iterator
+     *
+     * @return int
+     */
+    public function key()
+    {
+        return $this->_currentDn;
+    }
+
+    /**
+     * Move forward to next result item
+     * Implements Iterator
+     *
+     * @throws Zend_Ldap_Exception
+     */
+    public function next()
+    {
+        if (!is_resource($this->_current)) return;
+        $this->_current = @ldap_next_entry($this->_ldap->getResource(), $this->_current);
+        /**
+         * @see Zend_Ldap_Exception
+         */
+        require_once 'Zend/Ldap/Exception.php';
+        if ($this->_current === false) {
+            $msg = $this->_ldap->getLastError($code);
+            if ($code === Zend_Ldap_Exception::LDAP_SIZELIMIT_EXCEEDED) {
+                // we have reached the size limit enforced by the server
+                return;
+            } else if ($code > Zend_Ldap_Exception::LDAP_SUCCESS) {
+                 throw new Zend_Ldap_Exception($this->_ldap, 'getting next entry (' . $msg . ')');
+            }
+        }
+        $this->_storeCurrentDn();
+    }
+
+    /**
+     * Rewind the Iterator to the first result item
+     * Implements Iterator
+     *
+     * @throws Zend_Ldap_Exception
+     */
+    public function rewind()
+    {
+        if (!is_resource($this->_resultId)) return;
+        $this->_current = @ldap_first_entry($this->_ldap->getResource(), $this->_resultId);
+        /**
+         * @see Zend_Ldap_Exception
+         */
+        require_once 'Zend/Ldap/Exception.php';
+        if ($this->_current === false &&
+                $this->_ldap->getLastErrorCode() > Zend_Ldap_Exception::LDAP_SUCCESS) {
+            throw new Zend_Ldap_Exception($this->_ldap, 'getting first entry');
+        }
+
+        $this->_storeCurrentDn();
+    }
+
+    /**
+     * Stores the current DN
+     *
+     * @return void
+     * @throws Zend_Ldap_Exception
+     */
+    protected function _storeCurrentDn()
+    {
+        if (is_resource($this->_current)) {
+            $this->_currentDn = @ldap_get_dn($this->_ldap->getResource(), $this->_current);
+            if ($this->_currentDn === false) {
+                throw new Zend_Ldap_Exception($this->_ldap, 'getting dn');
+            }
+        } else {
+            $this->_currentDn = null;
+        }
+    }
+
+    /**
+     * Check if there is a current result item
+     * after calls to rewind() or next()
+     * Implements Iterator
+     *
+     * @return boolean
+     */
+    public function valid()
+    {
+        return (is_resource($this->_current) && is_string($this->_currentDn));
+    }
+
+}

+ 46 - 0
library/Zend/Ldap/Collection/Iterator/Interface.php

@@ -0,0 +1,46 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * Zend_Ldap_Collection_Iterator_Interface provides a contract for
+ * adapter specific collection iterators
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+interface Zend_Ldap_Collection_Iterator_Interface extends Iterator, Countable
+{
+    /**
+     * Closes the current result set
+     *
+     * @return boolean
+     */
+    public function close();
+
+    /**
+     * Gets the current LDAP connection.
+     *
+     * @return Zend_Ldap
+     */
+    public function getLdap();
+}

+ 71 - 0
library/Zend/Ldap/Converter.php

@@ -0,0 +1,71 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * Zend_Ldap_Converter is a collection of useful LDAP related conversion functions.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Converter
+{
+    /**
+     * Converts all ASCII chars < 32 to "\HEX"
+     *
+     * @see Net_LDAP2_Util::asc2hex32() from Benedikt Hallinger <beni@php.net>
+     * @link http://pear.php.net/package/Net_LDAP2
+     * @author Benedikt Hallinger <beni@php.net>
+     *
+     * @param  string $string String to convert
+     * @return string
+     */
+    public static function ascToHex32($string)
+    {
+        for ($i = 0; $i<strlen($string); $i++) {
+            $char = substr($string, $i, 1);
+            if (ord($char)<32) {
+                $hex = dechex(ord($char));
+                if (strlen($hex) == 1) $hex = '0' . $hex;
+                $string = str_replace($char, '\\' . $hex, $string);
+            }
+        }
+        return $string;
+    }
+
+    /**
+     * Converts all Hex expressions ("\HEX") to their original ASCII characters
+     *
+     * @see Net_LDAP2_Util::hex2asc() from Benedikt Hallinger <beni@php.net>,
+     * heavily based on work from DavidSmith@byu.net
+     * @link http://pear.php.net/package/Net_LDAP2
+     * @author Benedikt Hallinger <beni@php.net>, heavily based on work from DavidSmith@byu.net
+     *
+     * @param  string $string String to convert
+     * @return string
+     */
+    public static function hex32ToAsc($string)
+    {
+        $string = preg_replace("/\\\([0-9A-Fa-f]{2})/e", "''.chr(hexdec('\\1')).''", $string);
+        return $string;
+    }
+}

+ 794 - 0
library/Zend/Ldap/Dn.php

@@ -0,0 +1,794 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * Zend_Ldap_Dn provides an API for DN manipulation
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Dn implements ArrayAccess
+{
+    const ATTR_CASEFOLD_NONE  = 'none';
+    const ATTR_CASEFOLD_UPPER = 'upper';
+    const ATTR_CASEFOLD_LOWER = 'lower';
+
+    /**
+     * The default case fold to use
+     *
+     * @var string
+     */
+    protected static $_defaultCaseFold = self::ATTR_CASEFOLD_NONE;
+
+    /**
+     * The case fold used for this instance
+     *
+     * @var string
+     */
+    protected $_caseFold;
+
+    /**
+     * The DN data
+     *
+     * @var array
+     */
+    protected $_dn;
+
+    /**
+     * Creates a DN from an array or a string
+     *
+     * @param  string|array $dn
+     * @param  string|null  $caseFold
+     * @return Zend_Ldap_Dn
+     * @throws Zend_Ldap_Exception
+     */
+    public static function factory($dn, $caseFold = null)
+    {
+        if (is_array($dn)) {
+            return self::fromArray($dn, $caseFold);
+        } else if (is_string($dn)) {
+            return self::fromString($dn, $caseFold);
+        } else {
+            /**
+             * Zend_Ldap_Exception
+             */
+            require_once 'Zend/Ldap/Exception.php';
+            throw new Zend_Ldap_Exception(null, 'Invalid argument type for $dn');
+        }
+    }
+
+    /**
+     * Creates a DN from a string
+     *
+     * @param  string      $dn
+     * @param  string|null $caseFold
+     * @return Zend_Ldap_Dn
+     * @throws Zend_Ldap_Exception
+     */
+    public static function fromString($dn, $caseFold = null)
+    {
+        $dn = trim($dn);
+        if (empty($dn)) {
+            $dnArray = array();
+        } else {
+            $dnArray = self::explodeDn((string)$dn);
+        }
+        return new self($dnArray, $caseFold);
+    }
+
+    /**
+     * Creates a DN from an array
+     *
+     * @param  array       $dn
+     * @param  string|null $caseFold
+     * @return Zend_Ldap_Dn
+     * @throws Zend_Ldap_Exception
+     */
+    public static function fromArray(array $dn, $caseFold = null)
+    {
+         return new self($dn, $caseFold);
+    }
+
+    /**
+     * Constructor
+     *
+     * @param array       $dn
+     * @param string|null $caseFold
+     */
+    protected function __construct(array $dn, $caseFold)
+    {
+        $this->_dn = $dn;
+        $this->setCaseFold($caseFold);
+    }
+
+    /**
+     * Gets the RDN of the current DN
+     *
+     * @param  string $caseFold
+     * @return array
+     * @throws Zend_Ldap_Exception if DN has no RDN (empty array)
+     */
+    public function getRdn($caseFold = null)
+    {
+        $caseFold = self::_sanitizeCaseFold($caseFold, $this->_caseFold);
+        return self::_caseFoldRdn($this->get(0, 1, $caseFold), null);
+    }
+
+    /**
+     * Gets the RDN of the current DN as a string
+     *
+     * @param  string $caseFold
+     * @return string
+     * @throws Zend_Ldap_Exception if DN has no RDN (empty array)
+     */
+    public function getRdnString($caseFold = null)
+    {
+        $caseFold = self::_sanitizeCaseFold($caseFold, $this->_caseFold);
+        return self::implodeRdn($this->getRdn(), $caseFold);
+    }
+
+    /**
+     * Get the parent DN $levelUp levels up the tree
+     *
+     * @param int $levelUp
+     * @return Zend_Ldap_Dn
+     */
+    public function getParentDn($levelUp = 1)
+    {
+        $levelUp = (int)$levelUp;
+        if ($levelUp < 1 || $levelUp >= count($this->_dn)) {
+            /**
+             * Zend_Ldap_Exception
+             */
+            require_once 'Zend/Ldap/Exception.php';
+            throw new Zend_Ldap_Exception(null, 'Cannot retrieve parent DN with given $levelUp');
+        }
+        $newDn = array_slice($this->_dn, $levelUp);
+        return new self($newDn, $this->_caseFold);
+    }
+
+    /**
+     * Get a DN part
+     *
+     * @param int $index
+     * @param int $length
+     * @param string $caseFold
+     * @return array
+     * @throws Zend_Ldap_Exception if index is illegal
+     */
+    public function get($index, $length = 1, $caseFold = null)
+    {
+        $caseFold = self::_sanitizeCaseFold($caseFold, $this->_caseFold);
+        $this->_assertIndex($index);
+        $length = (int)$length;
+        if ($length <= 0) {
+            $length = 1;
+        }
+        if ($length === 1) {
+            return self::_caseFoldRdn($this->_dn[$index], $caseFold);
+        }
+        else {
+            return self::_caseFoldDn(array_slice($this->_dn, $index, $length, false), $caseFold);
+        }
+    }
+
+    /**
+     * Set a DN part
+     *
+     * @param int $index
+     * @param array $value
+     * @return Zend_Ldap_Dn Provides a fluent interface
+     * @throws Zend_Ldap_Exception if index is illegal
+     */
+    public function set($index, array $value)
+    {
+        $this->_assertIndex($index);
+        self::_assertRdn($value);
+        $this->_dn[$index] = $value;
+        return $this;
+    }
+
+    /**
+     * Remove a DN part
+     *
+     * @param int $index
+     * @param int $length
+     * @return Zend_Ldap_Dn Provides a fluent interface
+     * @throws Zend_Ldap_Exception if index is illegal
+     */
+    public function remove($index, $length = 1)
+    {
+        $this->_assertIndex($index);
+        $length = (int)$length;
+        if ($length <= 0) {
+            $length = 1;
+        }
+        array_splice($this->_dn, $index, $length, null);
+        return $this;
+    }
+
+    /**
+     * Append a DN part
+     *
+     * @param array $value
+     * @return Zend_Ldap_Dn Provides a fluent interface
+     */
+    public function append(array $value)
+    {
+        self::_assertRdn($value);
+        $this->_dn[] = $value;
+        return $this;
+    }
+
+    /**
+     * Prepend a DN part
+     *
+     * @param array $value
+     * @return Zend_Ldap_Dn Provides a fluent interface
+     */
+    public function prepend(array $value)
+    {
+        self::_assertRdn($value);
+        array_unshift($this->_dn, $value);
+        return $this;
+    }
+
+    /**
+     * Insert a DN part
+     *
+     * @param int index
+     * @param array $value
+     * @return Zend_Ldap_Dn Provides a fluent interface
+     * @throws Zend_Ldap_Exception if index is illegal
+     */
+    public function insert($index, array $value)
+    {
+        $this->_assertIndex($index);
+        self::_assertRdn($value);
+        $first = array_slice($this->_dn, 0, $index + 1);
+        $second = array_slice($this->_dn, $index + 1);
+        $this->_dn = array_merge($first, array($value), $second);
+        return $this;
+    }
+
+    /**
+     * Assert index is correct and usable
+     *
+     * @param mixed $index
+     * @return boolean
+     * @throws Zend_Ldap_Exception
+     */
+    protected function _assertIndex($index)
+    {
+        if (!is_int($index)) {
+            /**
+             * Zend_Ldap_Exception
+             */
+            require_once 'Zend/Ldap/Exception.php';
+            throw new Zend_Ldap_Exception(null, 'Parameter $index must be an integer');
+        }
+        if ($index < 0 || $index >= count($this->_dn)) {
+            /**
+             * Zend_Ldap_Exception
+             */
+            require_once 'Zend/Ldap/Exception.php';
+            throw new Zend_Ldap_Exception(null, 'Parameter $index out of bounds');
+        }
+        return true;
+    }
+
+    /**
+     * Assert if value is in a correct RDN format
+     *
+     * @param array $value
+     * @return boolean
+     * @throws Zend_Ldap_Exception
+     */
+    protected static function _assertRdn(array $value)
+    {
+        if (count($value)<1) {
+            /**
+             * Zend_Ldap_Exception
+             */
+            require_once 'Zend/Ldap/Exception.php';
+            throw new Zend_Ldap_Exception(null, 'RDN Array is malformed: it must have at least one item');
+        }
+
+        foreach (array_keys($value) as $key) {
+            if (!is_string($key)) {
+                /**
+                 * Zend_Ldap_Exception
+                 */
+                require_once 'Zend/Ldap/Exception.php';
+                throw new Zend_Ldap_Exception(null, 'RDN Array is malformed: it must use string keys');
+            }
+        }
+    }
+
+    /**
+     * Sets the case fold
+     *
+     * @param string|null $caseFold
+     */
+    public function setCaseFold($caseFold)
+    {
+        $this->_caseFold = self::_sanitizeCaseFold($caseFold, self::$_defaultCaseFold);
+    }
+
+    /**
+     * Return DN as a string
+     *
+     * @param string $caseFold
+     * @return string
+     * @throws Zend_Ldap_Exception
+     */
+    public function toString($caseFold = null)
+    {
+        $caseFold = self::_sanitizeCaseFold($caseFold, $this->_caseFold);
+        return self::implodeDn($this->_dn, $caseFold);
+    }
+
+    /**
+     * Return DN as an array
+     *
+     * @param string $caseFold
+     * @return array
+     */
+    public function toArray($caseFold = null)
+    {
+        $caseFold = self::_sanitizeCaseFold($caseFold, $this->_caseFold);
+
+        if ($caseFold === self::ATTR_CASEFOLD_NONE) {
+            return $this->_dn;
+        } else {
+            return self::_caseFoldDn($this->_dn, $caseFold);
+        }
+    }
+
+    /**
+     * Do a case folding on a RDN
+     *
+     * @param array $part
+     * @param string $caseFold
+     * @return array
+     */
+    protected static function _caseFoldRdn(array $part, $caseFold)
+    {
+        switch ($caseFold) {
+            case self::ATTR_CASEFOLD_UPPER:
+                return array_change_key_case($part, CASE_UPPER);
+            case self::ATTR_CASEFOLD_LOWER:
+                return array_change_key_case($part, CASE_LOWER);
+            case self::ATTR_CASEFOLD_NONE:
+            default:
+                return $part;
+        }
+    }
+
+    /**
+     * Do a case folding on a DN ort part of it
+     *
+     * @param array $dn
+     * @param string $caseFold
+     * @return array
+     */
+    protected static function _caseFoldDn(array $dn, $caseFold)
+    {
+        $return = array();
+        foreach ($dn as $part) {
+            $return[] = self::_caseFoldRdn($part, $caseFold);
+        }
+        return $return;
+    }
+
+    /**
+     * Cast to string representation {@see toString()}
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return $this->toString();
+    }
+
+    /**
+     * Required by the ArrayAccess implementation
+     *
+     * @param int $offset
+     * @return boolean
+     */
+    public function offsetExists($offset)
+    {
+        $offset = (int)$offset;
+        if ($offset < 0 || $offset >= count($this->_dn)) {
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * Proxy to {@see get()}
+     * Required by the ArrayAccess implementation
+     *
+     * @param int $offset
+     * @return array
+     */
+     public function offsetGet($offset)
+     {
+         return $this->get($offset, 1, null);
+     }
+
+     /**
+      * Proxy to {@see set()}
+      * Required by the ArrayAccess implementation
+      *
+      * @param int $offset
+      * @param array $value
+      */
+     public function offsetSet($offset, $value)
+     {
+         $this->set($offset, $value);
+     }
+
+     /**
+      * Proxy to {@see remove()}
+      * Required by the ArrayAccess implementation
+      *
+      * @param int $offset
+      */
+     public function offsetUnset($offset)
+     {
+         $this->remove($offset, 1);
+     }
+
+    /**
+     * Sets the default case fold
+     *
+     * @param string $caseFold
+     */
+    public static function setDefaultCaseFold($caseFold)
+    {
+        self::$_defaultCaseFold = self::_sanitizeCaseFold($caseFold, self::ATTR_CASEFOLD_NONE);
+    }
+
+    /**
+     * Sanitizes the case fold
+     *
+     * @param string $caseFold
+     * @return string
+     */
+    protected static function _sanitizeCaseFold($caseFold, $default)
+    {
+        switch ($caseFold) {
+            case self::ATTR_CASEFOLD_NONE:
+            case self::ATTR_CASEFOLD_UPPER:
+            case self::ATTR_CASEFOLD_LOWER:
+                return $caseFold;
+                break;
+            default:
+                return $default;
+                break;
+        }
+    }
+
+    /**
+     * Escapes a DN value according to RFC 2253
+     *
+     * Escapes the given VALUES according to RFC 2253 so that they can be safely used in LDAP DNs.
+     * The characters ",", "+", """, "\", "<", ">", ";", "#", " = " with a special meaning in RFC 2252
+     * are preceeded by ba backslash. Control characters with an ASCII code < 32 are represented as \hexpair.
+     * Finally all leading and trailing spaces are converted to sequences of \20.
+     * @see Net_LDAP2_Util::escape_dn_value() from Benedikt Hallinger <beni@php.net>
+     * @link http://pear.php.net/package/Net_LDAP2
+     * @author Benedikt Hallinger <beni@php.net>
+     *
+     * @param  string|array $values An array containing the DN values that should be escaped
+     * @return array The array $values, but escaped
+     */
+    public static function escapeValue($values = array())
+    {
+        /**
+         * @see Zend_Ldap_Converter
+         */
+        require_once 'Zend/Ldap/Converter.php';
+
+        if (!is_array($values)) $values = array($values);
+        foreach ($values as $key => $val) {
+            // Escaping of filter meta characters
+            $val = str_replace(array('\\', ',', '+', '"', '<', '>', ';', '#', '=', ),
+                array('\\\\', '\,', '\+', '\"', '\<', '\>', '\;', '\#', '\='), $val);
+            $val = Zend_Ldap_Converter::ascToHex32($val);
+
+            // Convert all leading and trailing spaces to sequences of \20.
+            if (preg_match('/^(\s*)(.+?)(\s*)$/', $val, $matches)) {
+                $val = $matches[2];
+                for ($i = 0; $i<strlen($matches[1]); $i++) {
+                    $val = '\20' . $val;
+                }
+                for ($i = 0; $i<strlen($matches[3]); $i++) {
+                    $val = $val . '\20';
+                }
+            }
+            if (null === $val) $val = '\0';  // apply escaped "null" if string is empty
+            $values[$key] = $val;
+        }
+        return (count($values) == 1) ? $values[0] : $values;
+    }
+
+    /**
+     * Undoes the conversion done by {@link escapeValue()}.
+     *
+     * Any escape sequence starting with a baskslash - hexpair or special character -
+     * will be transformed back to the corresponding character.
+     * @see Net_LDAP2_Util::escape_dn_value() from Benedikt Hallinger <beni@php.net>
+     * @link http://pear.php.net/package/Net_LDAP2
+     * @author Benedikt Hallinger <beni@php.net>
+     *
+     * @param  string|array $values Array of DN Values
+     * @return array Same as $values, but unescaped
+     */
+    public static function unescapeValue($values = array())
+    {
+        /**
+         * @see Zend_Ldap_Converter
+         */
+        require_once 'Zend/Ldap/Converter.php';
+
+        if (!is_array($values)) $values = array($values);
+        foreach ($values as $key => $val) {
+            // strip slashes from special chars
+            $val = str_replace(array('\\\\', '\,', '\+', '\"', '\<', '\>', '\;', '\#', '\='),
+                array('\\', ',', '+', '"', '<', '>', ';', '#', '=', ), $val);
+            $values[$key] = Zend_Ldap_Converter::hex32ToAsc($val);
+        }
+        return (count($values) == 1) ? $values[0] : $values;
+    }
+
+    /**
+     * Creates an array containing all parts of the given DN.
+     *
+     * Array will be of type
+     * array(
+     *      array("cn" => "name1", "uid" => "user"),
+     *      array("cn" => "name2"),
+     *      array("dc" => "example"),
+     *      array("dc" => "org")
+     * )
+     * for a DN of cn=name1+uid=user,cn=name2,dc=example,dc=org.
+     *
+     * @param  string $dn
+     * @param  array  $keys An optional array to receive DN keys (e.g. CN, OU, DC, ...)
+     * @param  array  $vals An optional array to receive DN values
+     * @param  string $caseFold
+     * @return array
+     * @throws Zend_Ldap_Exception
+     */
+    public static function explodeDn($dn, array &$keys = null, array &$vals = null,
+        $caseFold = self::ATTR_CASEFOLD_NONE)
+    {
+        $k = array();
+        $v = array();
+        if (!self::checkDn($dn, $k, $v, $caseFold)) {
+            /**
+             * Zend_Ldap_Exception
+             */
+            require_once 'Zend/Ldap/Exception.php';
+            throw new Zend_Ldap_Exception(null, 'DN is malformed');
+        }
+        $ret = array();
+        for ($i = 0; $i < count($k); $i++) {
+            if (is_array($k[$i]) && is_array($v[$i]) && (count($k[$i]) === count($v[$i]))) {
+                $multi = array();
+                for ($j = 0; $j < count($k[$i]); $j++) {
+                    $key=$k[$i][$j];
+                    $val=$v[$i][$j];
+                    $multi[$key] = $val;
+                }
+                $ret[] = $multi;
+            } else if (is_string($k[$i]) && is_string($v[$i])) {
+                $ret[] = array($k[$i] => $v[$i]);
+            }
+        }
+        if (!is_null($keys)) $keys = $k;
+        if (!is_null($vals)) $vals = $v;
+        return $ret;
+    }
+
+    /**
+     * @param  string $dn   The DN to parse
+     * @param  array  $keys An optional array to receive DN keys (e.g. CN, OU, DC, ...)
+     * @param  array  $vals An optional array to receive DN values
+     * @param  string $caseFold
+     * @return boolean True if the DN was successfully parsed or false if the string is not a valid DN.
+     */
+    public static function checkDn($dn, array &$keys = null, array &$vals = null,
+        $caseFold = self::ATTR_CASEFOLD_NONE)
+    {
+        /* This is a classic state machine parser. Each iteration of the
+         * loop processes one character. State 1 collects the key. When equals ( = )
+         * is encountered the state changes to 2 where the value is collected
+         * until a comma (,) or semicolon (;) is encountered after which we switch back
+         * to state 1. If a backslash (\) is encountered, state 3 is used to collect the
+         * following character without engaging the logic of other states.
+         */
+        $key = null;
+        $value = null;
+        $slen = strlen($dn);
+        $state = 1;
+        $ko = $vo = 0;
+        $multi = false;
+        $ka = array();
+        $va = array();
+        for ($di = 0; $di <= $slen; $di++) {
+            $ch = ($di == $slen) ? 0 : $dn[$di];
+            switch ($state) {
+                case 1: // collect key
+                    if ($ch === '=') {
+                        $key = trim(substr($dn, $ko, $di - $ko));
+                        if ($caseFold == self::ATTR_CASEFOLD_LOWER) $key = strtolower($key);
+                        else if ($caseFold == self::ATTR_CASEFOLD_UPPER) $key = strtoupper($key);
+                        if (is_array($multi)) {
+                            $keyId = strtolower($key);
+                            if (in_array($keyId, $multi)) {
+                                return false;
+                            }
+                            $ka[count($ka)-1][] = $key;
+                            $multi[] = $keyId;
+                        } else {
+                            $ka[] = $key;
+                        }
+                        $state = 2;
+                        $vo = $di + 1;
+                    } else if ($ch === ',' || $ch === ';' || $ch === '+') {
+                        return false;
+                    }
+                    break;
+                case 2: // collect value
+                    if ($ch === '\\') {
+                        $state = 3;
+                    } else if ($ch === ',' || $ch === ';' || $ch === 0 || $ch === '+') {
+                        $value = self::unescapeValue(trim(substr($dn, $vo, $di - $vo)));
+                        if (is_array($multi)) {
+                            $va[count($va)-1][] = $value;
+                        } else {
+                            $va[] = $value;
+                        }
+                        $state = 1;
+                        $ko = $di + 1;
+                        if ($ch === '+' && $multi === false) {
+                            $lastKey = array_pop($ka);
+                            $lastVal = array_pop($va);
+                            $ka[] = array($lastKey);
+                            $va[] = array($lastVal);
+                            $multi = array(strtolower($lastKey));
+                        } else if ($ch === ','|| $ch === ';' || $ch === 0) {
+                            $multi = false;
+                        }
+                    } else if ($ch === '=') {
+                        return false;
+                    }
+                    break;
+                case 3: // escaped
+                    $state = 2;
+                    break;
+            }
+        }
+
+        if ($keys !== null) {
+            $keys = $ka;
+        }
+        if ($vals !== null) {
+            $vals = $va;
+        }
+
+        return ($state === 1 && $ko > 0);
+    }
+
+    /**
+     * Returns a DN part in the form $attribute = $value
+     *
+     * This method supports the creation of multi-valued RDNs
+     * $part must contain an even number of elemets.
+     *
+     * @param  array $attribute
+     * @param  string $caseFold
+     * @return string
+     * @throws Zend_Ldap_Exception
+     */
+    public static function implodeRdn(array $part, $caseFold = null)
+    {
+        self::_assertRdn($part);
+        $part = self::_caseFoldRdn($part, $caseFold);
+        $rdnParts = array();
+        foreach ($part as $key => $value) {
+            $value = self::escapeValue($value);
+            $keyId = strtolower($key);
+            $rdnParts[$keyId] =  implode('=', array($key, $value));
+        }
+        ksort($rdnParts, SORT_STRING);
+        return implode('+', $rdnParts);
+    }
+
+    /**
+     * Implodes an array in the form delivered by {@link explodeDn()}
+     * to a DN string.
+     *
+     * $dnArray must be of type
+     * array(
+     *      array("cn" => "name1", "uid" => "user"),
+     *      array("cn" => "name2"),
+     *      array("dc" => "example"),
+     *      array("dc" => "org")
+     * )
+     *
+     * @param  array $dnArray
+     * @param  string $caseFold
+     * @param  string $separator
+     * @return string
+     * @throws Zend_Ldap_Exception
+     */
+    public static function implodeDn(array $dnArray, $caseFold = null, $separator = ',')
+    {
+        $parts = array();
+        foreach ($dnArray as $p) {
+            $parts[] = self::implodeRdn($p, $caseFold);
+        }
+        return implode($separator, $parts);
+    }
+
+    /**
+     * Checks if given $childDn is beneath $parentDn subtree.
+     *
+     * @param  string|Zend_Ldap_Dn $childDn
+     * @param  string|Zend_Ldap_Dn $parentDn
+     * @return boolean
+     */
+    public static function isChildOf($childDn, $parentDn)
+    {
+        try {
+            $keys = array();
+            $vals = array();
+            if ($childDn instanceof Zend_Ldap_Dn) {
+                $cdn = $childDn->toArray(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER);
+            } else {
+                $cdn = self::explodeDn($childDn, $keys, $vals, Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER);
+            }
+            if ($parentDn instanceof Zend_Ldap_Dn) {
+                $pdn = $parentDn->toArray(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER);
+            } else {
+                $pdn = self::explodeDn($parentDn, $keys, $vals, Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER);
+            }
+        }
+        catch (Zend_Ldap_Exception $e) {
+            return false;
+        }
+
+        $startIndex = count($cdn)-count($pdn);
+        if ($startIndex<0) return false;
+        for ($i = 0; $i<count($pdn); $i++) {
+            if ($cdn[$i+$startIndex] != $pdn[$i]) return false;
+        }
+        return true;
+    }
+}

+ 103 - 142
library/Zend/Ldap/Exception.php

@@ -27,175 +27,136 @@ require_once 'Zend/Exception.php';
 
 class Zend_Ldap_Exception extends Zend_Exception
 {
-    const LDAP_SUCCESS = 0x00;
-    const LDAP_OPERATIONS_ERROR = 0x01;
-    const LDAP_PROTOCOL_ERROR = 0x02;
-    const LDAP_TIMELIMIT_EXCEEDED = 0x03;
-    const LDAP_SIZELIMIT_EXCEEDED = 0x04;
-    const LDAP_COMPARE_FALSE = 0x05;
-    const LDAP_COMPARE_TRUE = 0x06;
-    const LDAP_AUTH_METHOD_NOT_SUPPORTED = 0x07;
-    const LDAP_STRONG_AUTH_REQUIRED = 0x08;
-    const LDAP_PARTIAL_RESULTS = 0x09;
-    const LDAP_REFERRAL = 0x0a;
-    const LDAP_ADMINLIMIT_EXCEEDED = 0x0b;
+    const LDAP_SUCCESS                        = 0x00;
+    const LDAP_OPERATIONS_ERROR               = 0x01;
+    const LDAP_PROTOCOL_ERROR                 = 0x02;
+    const LDAP_TIMELIMIT_EXCEEDED             = 0x03;
+    const LDAP_SIZELIMIT_EXCEEDED             = 0x04;
+    const LDAP_COMPARE_FALSE                  = 0x05;
+    const LDAP_COMPARE_TRUE                   = 0x06;
+    const LDAP_AUTH_METHOD_NOT_SUPPORTED      = 0x07;
+    const LDAP_STRONG_AUTH_REQUIRED           = 0x08;
+    const LDAP_PARTIAL_RESULTS                = 0x09;
+    const LDAP_REFERRAL                       = 0x0a;
+    const LDAP_ADMINLIMIT_EXCEEDED            = 0x0b;
     const LDAP_UNAVAILABLE_CRITICAL_EXTENSION = 0x0c;
-    const LDAP_CONFIDENTIALITY_REQUIRED = 0x0d;
-    const LDAP_SASL_BIND_IN_PROGRESS = 0x0e;
-    const LDAP_NO_SUCH_ATTRIBUTE = 0x10;
-    const LDAP_UNDEFINED_TYPE = 0x11;
-    const LDAP_INAPPROPRIATE_MATCHING = 0x12;
-    const LDAP_CONSTRAINT_VIOLATION = 0x13;
-    const LDAP_TYPE_OR_VALUE_EXISTS = 0x14;
-    const LDAP_INVALID_SYNTAX = 0x15;
-    const LDAP_NO_SUCH_OBJECT = 0x20;
-    const LDAP_ALIAS_PROBLEM = 0x21;
-    const LDAP_INVALID_DN_SYNTAX = 0x22;
-    const LDAP_IS_LEAF = 0x23;
-    const LDAP_ALIAS_DEREF_PROBLEM = 0x24;
-    const LDAP_PROXY_AUTHZ_FAILURE = 0x2F;
-    const LDAP_INAPPROPRIATE_AUTH = 0x30;
-    const LDAP_INVALID_CREDENTIALS = 0x31;
-    const LDAP_INSUFFICIENT_ACCESS = 0x32;
-    const LDAP_BUSY = 0x33;
-    const LDAP_UNAVAILABLE = 0x34;
-    const LDAP_UNWILLING_TO_PERFORM = 0x35;
-    const LDAP_LOOP_DETECT = 0x36;
-    const LDAP_NAMING_VIOLATION = 0x40;
-    const LDAP_OBJECT_CLASS_VIOLATION = 0x41;
-    const LDAP_NOT_ALLOWED_ON_NONLEAF = 0x42;
-    const LDAP_NOT_ALLOWED_ON_RDN = 0x43;
-    const LDAP_ALREADY_EXISTS = 0x44;
-    const LDAP_NO_OBJECT_CLASS_MODS = 0x45;
-    const LDAP_RESULTS_TOO_LARGE = 0x46;
-    const LDAP_AFFECTS_MULTIPLE_DSAS = 0x47;
-    const LDAP_OTHER = 0x50;
-    const LDAP_SERVER_DOWN = 0x51;
-    const LDAP_LOCAL_ERROR = 0x52;
-    const LDAP_ENCODING_ERROR = 0x53;
-    const LDAP_DECODING_ERROR = 0x54;
-    const LDAP_TIMEOUT = 0x55;
-    const LDAP_AUTH_UNKNOWN = 0x56;
-    const LDAP_FILTER_ERROR = 0x57;
-    const LDAP_USER_CANCELLED = 0x58;
-    const LDAP_PARAM_ERROR = 0x59;
-    const LDAP_NO_MEMORY = 0x5a;
-    const LDAP_CONNECT_ERROR = 0x5b;
-    const LDAP_NOT_SUPPORTED = 0x5c;
-    const LDAP_CONTROL_NOT_FOUND = 0x5d;
-    const LDAP_NO_RESULTS_RETURNED = 0x5e;
-    const LDAP_MORE_RESULTS_TO_RETURN = 0x5f;
-    const LDAP_CLIENT_LOOP = 0x60;
-    const LDAP_REFERRAL_LIMIT_EXCEEDED = 0x61;
-    const LDAP_CUP_RESOURCES_EXHAUSTED = 0x71;
-    const LDAP_CUP_SECURITY_VIOLATION = 0x72;
-    const LDAP_CUP_INVALID_DATA = 0x73;
-    const LDAP_CUP_UNSUPPORTED_SCHEME = 0x74;
-    const LDAP_CUP_RELOAD_REQUIRED = 0x75;
-    const LDAP_CANCELLED = 0x76;
-    const LDAP_NO_SUCH_OPERATION = 0x77;
-    const LDAP_TOO_LATE = 0x78;
-    const LDAP_CANNOT_CANCEL = 0x79;
-    const LDAP_ASSERTION_FAILED = 0x7A;
-    const LDAP_SYNC_REFRESH_REQUIRED = 0x1000;
-    const LDAP_X_SYNC_REFRESH_REQUIRED = 0x4100;
-    const LDAP_X_NO_OPERATION = 0x410e;
-    const LDAP_X_ASSERTION_FAILED = 0x410f;
-    const LDAP_X_NO_REFERRALS_FOUND = 0x4110;
-    const LDAP_X_CANNOT_CHAIN = 0x4111;
+    const LDAP_CONFIDENTIALITY_REQUIRED       = 0x0d;
+    const LDAP_SASL_BIND_IN_PROGRESS          = 0x0e;
+    const LDAP_NO_SUCH_ATTRIBUTE              = 0x10;
+    const LDAP_UNDEFINED_TYPE                 = 0x11;
+    const LDAP_INAPPROPRIATE_MATCHING         = 0x12;
+    const LDAP_CONSTRAINT_VIOLATION           = 0x13;
+    const LDAP_TYPE_OR_VALUE_EXISTS           = 0x14;
+    const LDAP_INVALID_SYNTAX                 = 0x15;
+    const LDAP_NO_SUCH_OBJECT                 = 0x20;
+    const LDAP_ALIAS_PROBLEM                  = 0x21;
+    const LDAP_INVALID_DN_SYNTAX              = 0x22;
+    const LDAP_IS_LEAF                        = 0x23;
+    const LDAP_ALIAS_DEREF_PROBLEM            = 0x24;
+    const LDAP_PROXY_AUTHZ_FAILURE            = 0x2F;
+    const LDAP_INAPPROPRIATE_AUTH             = 0x30;
+    const LDAP_INVALID_CREDENTIALS            = 0x31;
+    const LDAP_INSUFFICIENT_ACCESS            = 0x32;
+    const LDAP_BUSY                           = 0x33;
+    const LDAP_UNAVAILABLE                    = 0x34;
+    const LDAP_UNWILLING_TO_PERFORM           = 0x35;
+    const LDAP_LOOP_DETECT                    = 0x36;
+    const LDAP_NAMING_VIOLATION               = 0x40;
+    const LDAP_OBJECT_CLASS_VIOLATION         = 0x41;
+    const LDAP_NOT_ALLOWED_ON_NONLEAF         = 0x42;
+    const LDAP_NOT_ALLOWED_ON_RDN             = 0x43;
+    const LDAP_ALREADY_EXISTS                 = 0x44;
+    const LDAP_NO_OBJECT_CLASS_MODS           = 0x45;
+    const LDAP_RESULTS_TOO_LARGE              = 0x46;
+    const LDAP_AFFECTS_MULTIPLE_DSAS          = 0x47;
+    const LDAP_OTHER                          = 0x50;
+    const LDAP_SERVER_DOWN                    = 0x51;
+    const LDAP_LOCAL_ERROR                    = 0x52;
+    const LDAP_ENCODING_ERROR                 = 0x53;
+    const LDAP_DECODING_ERROR                 = 0x54;
+    const LDAP_TIMEOUT                        = 0x55;
+    const LDAP_AUTH_UNKNOWN                   = 0x56;
+    const LDAP_FILTER_ERROR                   = 0x57;
+    const LDAP_USER_CANCELLED                 = 0x58;
+    const LDAP_PARAM_ERROR                    = 0x59;
+    const LDAP_NO_MEMORY                      = 0x5a;
+    const LDAP_CONNECT_ERROR                  = 0x5b;
+    const LDAP_NOT_SUPPORTED                  = 0x5c;
+    const LDAP_CONTROL_NOT_FOUND              = 0x5d;
+    const LDAP_NO_RESULTS_RETURNED            = 0x5e;
+    const LDAP_MORE_RESULTS_TO_RETURN         = 0x5f;
+    const LDAP_CLIENT_LOOP                    = 0x60;
+    const LDAP_REFERRAL_LIMIT_EXCEEDED        = 0x61;
+    const LDAP_CUP_RESOURCES_EXHAUSTED        = 0x71;
+    const LDAP_CUP_SECURITY_VIOLATION         = 0x72;
+    const LDAP_CUP_INVALID_DATA               = 0x73;
+    const LDAP_CUP_UNSUPPORTED_SCHEME         = 0x74;
+    const LDAP_CUP_RELOAD_REQUIRED            = 0x75;
+    const LDAP_CANCELLED                      = 0x76;
+    const LDAP_NO_SUCH_OPERATION              = 0x77;
+    const LDAP_TOO_LATE                       = 0x78;
+    const LDAP_CANNOT_CANCEL                  = 0x79;
+    const LDAP_ASSERTION_FAILED               = 0x7A;
+    const LDAP_SYNC_REFRESH_REQUIRED          = 0x1000;
+    const LDAP_X_SYNC_REFRESH_REQUIRED        = 0x4100;
+    const LDAP_X_NO_OPERATION                 = 0x410e;
+    const LDAP_X_ASSERTION_FAILED             = 0x410f;
+    const LDAP_X_NO_REFERRALS_FOUND           = 0x4110;
+    const LDAP_X_CANNOT_CHAIN                 = 0x4111;
 
     /* internal error code constants */
 
-    const LDAP_X_DOMAIN_MISMATCH = 0x7001;
+    const LDAP_X_DOMAIN_MISMATCH              = 0x7001;
+    const LDAP_X_EXTENSION_NOT_LOADED         = 0x7002;
 
     /**
-     * @param mixed $ldap A Zend_Ldap object or raw LDAP context resource
+     * @param Zend_Ldap $ldap A Zend_Ldap object
      * @param string $str An informtive exception message
      * @param int $code An LDAP error code
      */
-    public function __construct($ldap = null, $str = null, $code = 0)
+    public function __construct(Zend_Ldap $ldap = null, $str = null, $code = 0)
     {
-        $resource = null;
-        if (is_resource($ldap)) {
-            $resource = $ldap;
-        } else if (is_object($ldap)) {
-            $resource = $ldap->getResource();
-        }
-
+        $errorMessages = array();
         $message = '';
-        if ($code === 0)
-            $code = Zend_Ldap_Exception::getLdapCode($resource);
-        if ($code)
-            $message .= '0x' . dechex($code);
-        if (is_resource($resource)) {
-
-            /* The various error retrieval functions can return
-             * different things so we just try to collect what we
-             * can and eliminate dupes.
-             */
-
-            $estr1 = @ldap_error($resource);
-            if ($code !== 0 && $estr1 === 'Success')
-                $estr1 = @ldap_err2str($code);
-            if ($estr1 !== $str)
-                $this->_append($message, $estr1);
-
-            @ldap_get_option($resource, LDAP_OPT_ERROR_STRING, $estr2);
-            if ($estr2 !== $str && $estr2 !== $estr1)
-                $this->_append($message, $estr2);
+        if ($ldap !== null) {
+            $oldCode = $code;
+            $message = $ldap->getLastError($code, $errorMessages) . ': ';
+            if ($code === 0) {
+                $message = '';
+                $code = $oldCode;
+            }
+        }
+        if (empty($message)) {
+            if ($code > 0) {
+                $message = '0x' . dechex($code) . ': ';
+            }
         }
 
-        $this->_append($message, $str);
+        if (!empty($str)) {
+            $message .= $str;
+        } else {
+            $message .= 'no exception message';
+        }
 
         parent::__construct($message, $code);
     }
 
-    private function _append(&$message, $estr)
-    {
-        if ($estr) {
-            if ($message)
-                $message .= ': ';
-            $message .= $estr;
-        }
-    }
 
     /**
-     * @param mixed $ldap A Zend_Ldap object or raw LDAP context resource
+     * @deprecated not necessary any more - will be removed
+     * @param Zend_Ldap $ldap A Zend_Ldap object
      * @return int The current error code for the resource
      */
-    public static function getLdapCode($ldap)
+    public static function getLdapCode(Zend_Ldap $ldap = null)
     {
-        $resource = null;
-        if (is_resource($ldap)) {
-            $resource = $ldap;
-        } else if (is_object($ldap)) {
-            $resource = $ldap->getResource();
-        }
-        if (is_resource($resource)) {
-            $ret = @ldap_get_option($resource, LDAP_OPT_ERROR_NUMBER, $err);
-            if ($ret === true) {
-                if ($err <= -1 && $err >= -17) {
-                    /* For some reason draft-ietf-ldapext-ldap-c-api-xx.txt error
-                     * codes in OpenLDAP are negative values from -1 to -17. 
-                     */
-                    $err = Zend_Ldap_Exception::LDAP_SERVER_DOWN + (-$err - 1);
-                }
-                return $err;
-            }
+        if ($ldap !== null) {
+            return $ldap->getLastErrorCode();
         }
         return 0;
     }
 
     /**
-     * @param string $message An informtive exception message
-     *
-     */
-    public function setMessage($message)
-    {
-        $this->_message = $message;
-    }
-
-    /**
+     * @deprecated will be removed
      * @return int The current error code for this exception
      */
     public function getErrorCode()

+ 265 - 0
library/Zend/Ldap/Filter.php

@@ -0,0 +1,265 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Filter
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Ldap_Filter_String
+ */
+require_once 'Zend/Ldap/Filter/String.php';
+
+/**
+ * Zend_Ldap_Filter.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Filter
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Filter extends Zend_Ldap_Filter_String
+{
+    const TYPE_EQUALS = '=';
+    const TYPE_GREATER = '>';
+    const TYPE_GREATEROREQUAL = '>=';
+    const TYPE_LESS = '<';
+    const TYPE_LESSOREQUAL = '<=';
+    const TYPE_APPROX = '~=';
+
+    /**
+     * Creates an 'equals' filter.
+     * (attr=value)
+     *
+     * @param  string $attr
+     * @param  string $value
+     * @return Zend_Ldap_Filter
+     */
+    public static function equals($attr, $value)
+    {
+        return new self($attr, $value, self::TYPE_EQUALS, null, null);
+    }
+
+    /**
+     * Creates a 'begins with' filter.
+     * (attr=value*)
+     *
+     * @param  string $attr
+     * @param  string $value
+     * @return Zend_Ldap_Filter
+     */
+    public static function begins($attr, $value)
+    {
+        return new self($attr, $value, self::TYPE_EQUALS, null, '*');
+    }
+
+    /**
+     * Creates an 'ends with' filter.
+     * (attr=*value)
+     *
+     * @param  string $attr
+     * @param  string $value
+     * @return Zend_Ldap_Filter
+     */
+    public static function ends($attr, $value)
+    {
+        return new self($attr, $value, self::TYPE_EQUALS, '*', null);
+    }
+
+    /**
+     * Creates a 'contains' filter.
+     * (attr=*value*)
+     *
+     * @param  string $attr
+     * @param  string $value
+     * @return Zend_Ldap_Filter
+     */
+    public static function contains($attr, $value)
+    {
+        return new self($attr, $value, self::TYPE_EQUALS, '*', '*');
+    }
+
+    /**
+     * Creates a 'greater' filter.
+     * (attr>value)
+     *
+     * @param  string $attr
+     * @param  string $value
+     * @return Zend_Ldap_Filter
+     */
+    public static function greater($attr, $value)
+    {
+        return new self($attr, $value, self::TYPE_GREATER, null, null);
+    }
+
+    /**
+     * Creates a 'greater or equal' filter.
+     * (attr>=value)
+     *
+     * @param  string $attr
+     * @param  string $value
+     * @return Zend_Ldap_Filter
+     */
+    public static function greaterOrEqual($attr, $value)
+    {
+        return new self($attr, $value, self::TYPE_GREATEROREQUAL, null, null);
+    }
+
+    /**
+     * Creates a 'less' filter.
+     * (attr<value)
+     *
+     * @param  string $attr
+     * @param  string $value
+     * @return Zend_Ldap_Filter
+     */
+    public static function less($attr, $value)
+    {
+        return new self($attr, $value, self::TYPE_LESS, null, null);
+    }
+
+    /**
+     * Creates an 'less or equal' filter.
+     * (attr<=value)
+     *
+     * @param  string $attr
+     * @param  string $value
+     * @return Zend_Ldap_Filter
+     */
+    public static function lessOrEqual($attr, $value)
+    {
+        return new self($attr, $value, self::TYPE_LESSOREQUAL, null, null);
+    }
+
+    /**
+     * Creates an 'approx' filter.
+     * (attr~=value)
+     *
+     * @param  string $attr
+     * @param  string $value
+     * @return Zend_Ldap_Filter
+     */
+    public static function approx($attr, $value)
+    {
+        return new self($attr, $value, self::TYPE_APPROX, null, null);
+    }
+
+    /**
+     * Creates an 'any' filter.
+     * (attr=*)
+     *
+     * @param  string $attr
+     * @return Zend_Ldap_Filter
+     */
+    public static function any($attr)
+    {
+        return new self($attr, '', self::TYPE_EQUALS, '*', null);
+    }
+
+    /**
+     * Creates a simple custom string filter.
+     *
+     * @param  string $filter
+     * @return Zend_Ldap_Filter_String
+     */
+    public static function string($filter)
+    {
+        return new Zend_Ldap_Filter_String($filter);
+    }
+
+    /**
+     * Creates a simple string filter to be used with a mask.
+     *
+     * @param string $mask
+     * @param string $value
+     * @return Zend_Ldap_Filter_Mask
+     */
+    public static function mask($mask, $value)
+    {
+        /**
+         * Zend_Ldap_Filter_Mask
+         */
+        require_once 'Zend/Ldap/Filter/Mask.php';
+        return new Zend_Ldap_Filter_Mask($mask, $value);
+    }
+
+    /**
+     * Creates an 'and' filter.
+     *
+     * @param  Zend_Ldap_Filter_Abstract $filter,...
+     * @return Zend_Ldap_Filter_And
+     */
+    public static function andFilter($filter)
+    {
+        /**
+         * Zend_Ldap_Filter_And
+         */
+        require_once 'Zend/Ldap/Filter/And.php';
+        return new Zend_Ldap_Filter_And(func_get_args());
+    }
+
+    /**
+     * Creates an 'or' filter.
+     *
+     * @param  Zend_Ldap_Filter_Abstract $filter,...
+     * @return Zend_Ldap_Filter_Or
+     */
+    public static function orFilter($filter)
+    {
+        /**
+         * Zend_Ldap_Filter_Or
+         */
+        require_once 'Zend/Ldap/Filter/Or.php';
+        return new Zend_Ldap_Filter_Or(func_get_args());
+    }
+
+    /**
+     * Create a filter string.
+     *
+     * @param  string $attr
+     * @param  string $value
+     * @param  string $filtertype
+     * @param  string $prepend
+     * @param  string $append
+     * @return string
+     */
+    private static function _createFilterString($attr, $value, $filtertype, $prepend = null, $append = null)
+    {
+        $str = $attr . $filtertype;
+        if (!is_null($prepend)) $str .= $prepend;
+        $str .= self::escapeValue($value);
+        if (!is_null($append)) $str .= $append;
+        return $str;
+    }
+
+    /**
+     * Creates a new Zend_Ldap_Filter.
+     *
+     * @param string $attr
+     * @param string $value
+     * @param string $filtertype
+     * @param string $prepend
+     * @param string $append
+     */
+    public function __construct($attr, $value, $filtertype, $prepend = null, $append = null)
+    {
+        $filter = self::_createFilterString($attr, $value, $filtertype, $prepend, $append);
+        parent::__construct($filter);
+    }
+}

+ 157 - 0
library/Zend/Ldap/Filter/Abstract.php

@@ -0,0 +1,157 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Filter
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * Zend_Ldap_Filter_Abstract provides a base implementation for filters.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Filter
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+abstract class Zend_Ldap_Filter_Abstract
+{
+    /**
+     * Returns a string representation of the filter.
+     *
+     * @return string
+     */
+    abstract public function toString();
+
+    /**
+     * Returns a string representation of the filter.
+     * @see toString()
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return $this->toString();
+    }
+
+    /**
+     * Negates the filter.
+     *
+     * @return Zend_Ldap_Filter_Abstract
+     */
+    public function negate()
+    {
+        /**
+         * Zend_Ldap_Filter_Not
+         */
+        require_once 'Zend/Ldap/Filter/Not.php';
+        return new Zend_Ldap_Filter_Not($this);
+    }
+
+    /**
+     * Creates an 'and' filter.
+     *
+     * @param  Zend_Ldap_Filter_Abstract $filter,...
+     * @return Zend_Ldap_Filter_And
+     */
+    public function addAnd($filter)
+    {
+        /**
+         * Zend_Ldap_Filter_And
+         */
+        require_once 'Zend/Ldap/Filter/And.php';
+        $fa = func_get_args();
+        $args = array_merge(array($this), $fa);
+        return new Zend_Ldap_Filter_And($args);
+    }
+
+    /**
+     * Creates an 'or' filter.
+     *
+     * @param  Zend_Ldap_Filter_Abstract $filter,...
+     * @return Zend_Ldap_Filter_Or
+     */
+    public function addOr($filter)
+    {
+        /**
+         * Zend_Ldap_Filter_Or
+         */
+        require_once 'Zend/Ldap/Filter/Or.php';
+        $fa = func_get_args();
+        $args = array_merge(array($this), $fa);
+        return new Zend_Ldap_Filter_Or($args);
+    }
+
+    /**
+     * Escapes the given VALUES according to RFC 2254 so that they can be safely used in LDAP filters.
+     *
+     * Any control characters with an ACII code < 32 as well as the characters with special meaning in
+     * LDAP filters "*", "(", ")", and "\" (the backslash) are converted into the representation of a
+     * backslash followed by two hex digits representing the hexadecimal value of the character.
+     * @see Net_LDAP2_Util::escape_filter_value() from Benedikt Hallinger <beni@php.net>
+     * @link http://pear.php.net/package/Net_LDAP2
+     * @author Benedikt Hallinger <beni@php.net>
+     *
+     * @param  string|array $values Array of values to escape
+     * @return array Array $values, but escaped
+     */
+    public static function escapeValue($values = array())
+    {
+        /**
+         * @see Zend_Ldap_Converter
+         */
+        require_once 'Zend/Ldap/Converter.php';
+
+        if (!is_array($values)) $values = array($values);
+        foreach ($values as $key => $val) {
+            // Escaping of filter meta characters
+            $val = str_replace(array('\\', '*', '(', ')'), array('\5c', '\2a', '\28', '\29'), $val);
+            // ASCII < 32 escaping
+            $val = Zend_Ldap_Converter::ascToHex32($val);
+            if (null === $val) $val = '\0';  // apply escaped "null" if string is empty
+            $values[$key] = $val;
+        }
+        return (count($values) == 1) ? $values[0] : $values;
+    }
+
+    /**
+     * Undoes the conversion done by {@link escapeValue()}.
+     *
+     * Converts any sequences of a backslash followed by two hex digits into the corresponding character.
+     * @see Net_LDAP2_Util::escape_filter_value() from Benedikt Hallinger <beni@php.net>
+     * @link http://pear.php.net/package/Net_LDAP2
+     * @author Benedikt Hallinger <beni@php.net>
+     *
+     * @param  string|array $values Array of values to escape
+     * @return array Array $values, but unescaped
+     */
+    public static function unescapeValue($values = array())
+    {
+        /**
+         * @see Zend_Ldap_Converter
+         */
+        require_once 'Zend/Ldap/Converter.php';
+
+        if (!is_array($values)) $values = array($values);
+        foreach ($values as $key => $value) {
+            // Translate hex code into ascii
+            $values[$key] = Zend_Ldap_Converter::hex32ToAsc($value);
+        }
+        return (count($values) == 1) ? $values[0] : $values;
+    }
+}

+ 48 - 0
library/Zend/Ldap/Filter/And.php

@@ -0,0 +1,48 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Filter
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Ldap_Filter_Logical
+ */
+require_once 'Zend/Ldap/Filter/Logical.php';
+
+/**
+ * Zend_Ldap_Filter_And provides an 'and' filter.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Filter
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Filter_And extends Zend_Ldap_Filter_Logical  
+{
+    /**
+     * Creates an 'and' grouping filter.
+     *
+     * @param array $subfilters
+     */
+    public function __construct(array $subfilters)
+    {
+        parent::__construct($subfilters, self::TYPE_AND);
+    }
+}

+ 37 - 0
library/Zend/Ldap/Filter/Exception.php

@@ -0,0 +1,37 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Filter
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Exception
+ */
+require_once 'Zend/Exception.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Filter
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Filter_Exception extends Zend_Exception
+{
+}

+ 107 - 0
library/Zend/Ldap/Filter/Logical.php

@@ -0,0 +1,107 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Filter
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/** 
+ * @see Zend_Ldap_Filter_Abstract
+ */
+require_once 'Zend/Ldap/Filter/Abstract.php';
+/** 
+ * @see Zend_Ldap_Filter_String
+ */
+require_once 'Zend/Ldap/Filter/String.php';
+
+/**
+ * Zend_Ldap_Filter_Logical provides a base implementation for a grouping filter.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Filter
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+abstract class Zend_Ldap_Filter_Logical extends Zend_Ldap_Filter_Abstract  
+{
+    const TYPE_AND = '&';
+    const TYPE_OR = '|';
+    
+    /**
+     * All the sub-filters for this grouping filter.
+     *
+     * @var array
+     */
+    private $_subfilters;
+    
+    /**
+     * The grouping symbol.
+     *
+     * @var string
+     */
+    private $_symbol;
+    
+    /**
+     * Creates a new grouping filter.
+     *
+     * @param array $subfilters
+     * @param string $symbol
+     */
+    protected function __construct(array $subfilters, $symbol)
+    {
+        foreach ($subfilters as $key => $s) {
+            if (is_string($s)) $subfilters[$key] = new Zend_Ldap_Filter_String($s);
+            else if (!($s instanceof Zend_Ldap_Filter_Abstract)) {
+                /** 
+                 * @see Zend_Ldap_Filter_Exception
+                 */
+                require_once 'Zend/Ldap/Filter/Exception.php';
+                throw new Zend_Ldap_Filter_Exception('Only strings or Zend_Ldap_Filter_Abstract allowed.');
+            }
+        }
+        $this->_subfilters = $subfilters;
+        $this->_symbol = $symbol;
+    }
+    
+    /**
+     * Adds a filter to this grouping filter.
+     *
+     * @param Zend_Ldap_Filter_Abstract $filter
+     * @return Zend_Ldap_Filter_Logical 
+     */
+    public function addFilter(Zend_Ldap_Filter_Abstract $filter)
+    {
+        $new = clone $this;
+        $new->_subfilters[] = $filter;
+        return $new;         
+    }
+    
+    /**
+     * Returns a string representation of the filter.
+     *
+     * @return string
+     */
+    public function toString()
+    {
+        $return = '(' . $this->_symbol;
+        foreach ($this->_subfilters as $sub) $return .= $sub->toString();
+        $return .= ')';
+        return $return;
+    }
+}

+ 66 - 0
library/Zend/Ldap/Filter/Mask.php

@@ -0,0 +1,66 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Filter
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Ldap_Filter_String
+ */
+require_once 'Zend/Ldap/Filter/String.php';
+
+
+/**
+ * Zend_Ldap_Filter_Mask provides a simple string filter to be used with a mask.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Filter
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Filter_Mask extends Zend_Ldap_Filter_String
+{
+    /**
+     * Creates a Zend_Ldap_Filter_String.
+     *
+     * @param string $mask
+     * @param string $value,...
+     */
+    public function __construct($mask, $value)
+    {
+        $args = func_get_args();
+        array_shift($args);
+        for ($i = 0; $i<count($args); $i++) {
+            $args[$i] = self::escapeValue($args[$i]);
+        }
+        $filter = vsprintf($mask, $args);
+        parent::__construct($filter);
+    }
+
+    /**
+     * Returns a string representation of the filter.
+     *
+     * @return string
+     */
+    public function toString()
+    {
+        return $this->_filter;
+    }
+}

+ 75 - 0
library/Zend/Ldap/Filter/Not.php

@@ -0,0 +1,75 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Filter
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Ldap_Filter_Abstract
+ */
+require_once 'Zend/Ldap/Filter/Abstract.php';
+
+/**
+ * Zend_Ldap_Filter_Not provides a negation filter.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Filter
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Filter_Not extends Zend_Ldap_Filter_Abstract
+{
+    /**
+     * The underlying filter.
+     *
+     * @var Zend_Ldap_Filter_Abstract
+     */
+    private $_filter;
+
+    /**
+     * Creates a Zend_Ldap_Filter_Not.
+     *
+     * @param Zend_Ldap_Filter_Abstract $filter
+     */
+    public function __construct(Zend_Ldap_Filter_Abstract $filter)
+    {
+        $this->_filter = $filter;
+    }
+
+    /**
+     * Negates the filter.
+     *
+     * @return Zend_Ldap_Filter_Abstract
+     */
+    public function negate()
+    {
+        return $this->_filter;
+    }
+
+    /**
+     * Returns a string representation of the filter.
+     *
+     * @return string
+     */
+    public function toString()
+    {
+        return '(!' . $this->_filter->toString() . ')';
+    }
+}

+ 48 - 0
library/Zend/Ldap/Filter/Or.php

@@ -0,0 +1,48 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Filter
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/** 
+ * @see Zend_Ldap_Filter_Logical
+ */
+require_once 'Zend/Ldap/Filter/Logical.php';
+
+/**
+ * Zend_Ldap_Filter_Or provides an 'or' filter.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Filter
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Filter_Or extends Zend_Ldap_Filter_Logical  
+{
+    /**
+     * Creates an 'or' grouping filter.
+     *
+     * @param array $subfilters
+     */
+    public function __construct(array $subfilters)
+    {
+        parent::__construct($subfilters, self::TYPE_OR);
+    }
+}

+ 65 - 0
library/Zend/Ldap/Filter/String.php

@@ -0,0 +1,65 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Filter
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/** 
+ * @see Zend_Ldap_Filter_Abstract
+ */
+require_once 'Zend/Ldap/Filter/Abstract.php';
+
+/**
+ * Zend_Ldap_Filter_String provides a simple custom string filter.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Filter
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Filter_String extends Zend_Ldap_Filter_Abstract  
+{
+    /**
+     * The filter.
+     *
+     * @var string
+     */
+    protected $_filter;
+    
+    /**
+     * Creates a Zend_Ldap_Filter_String.
+     *
+     * @param string $filter
+     */
+    public function __construct($filter)
+    {
+        $this->_filter = $filter;
+    }
+    
+    /**
+     * Returns a string representation of the filter.
+     *
+     * @return string
+     */
+    public function toString()
+    {
+        return '(' . $this->_filter . ')';
+    }
+}

+ 301 - 0
library/Zend/Ldap/Ldif/Encoder.php

@@ -0,0 +1,301 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Ldif
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * Zend_Ldap_Ldif_Encoder provides methods to encode and decode LDAP data into/from LDIF.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Ldif
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Ldif_Encoder
+{
+    /**
+     * Additional options used during encoding
+     *
+     * @var array
+     */
+    protected $_options = array(
+        'sort'    => true,
+        'version' => 1,
+        'wrap'    => 78
+    );
+
+    protected $_versionWritten = false;
+
+    /**
+     * Constructor.
+     *
+     * @param array $options Additional options used during encoding
+     * @return void
+     */
+    protected function __construct(array $options = array())
+    {
+        $this->_options = array_merge($this->_options, $options);
+    }
+
+    /**
+     * Decodes the string $string into an array of LDIF items
+     *
+     * @param string $string
+     * @return array
+     */
+    public static function decode($string)
+    {
+        $encoder = new self(array());
+        return $encoder->_decode($string);
+    }
+
+    /**
+     * Decodes the string $string into an array of LDIF items
+     *
+     * @param string $string
+     * @return array
+     */
+    protected function _decode($string)
+    {
+        $items = array();
+        $item = array();
+        $last = null;
+        foreach (explode("\n", $string) as $line) {
+            $line = rtrim($line, "\x09\x0A\x0D\x00\x0B");
+            $matches = array();
+            if (substr($line, 0, 1) === ' ' && $last !== null) {
+                $last[2] .= substr($line, 1);
+            } else if (substr($line, 0, 1) === '#') {
+                continue;
+            } else if (preg_match('/^([a-z0-9;-]+)(:[:<]?\s*)([^:<]*)$/i', $line, $matches)) {
+                $name = strtolower($matches[1]);
+                $type = trim($matches[2]);
+                $value = $matches[3];
+                if ($last !== null) {
+                    $this->_pushAttribute($last, $item);
+                }
+                if ($name === 'version') {
+                    continue;
+                } else if (count($item) > 0 && $name === 'dn') {
+                    $items[] = $item;
+                    $item = array();
+                    $last = null;
+                }
+                $last = array($name, $type, $value);
+            } else if (trim($line) === '') {
+                continue;
+            }
+        }
+        if ($last !== null) {
+            $this->_pushAttribute($last, $item);
+        }
+        $items[] = $item;
+        return (count($items)>1) ? $items : $items[0];
+    }
+
+    /**
+     * Pushes a decoded attribute to the stack
+     *
+     * @param array $attribute
+     * @param array $entry
+     */
+    protected function _pushAttribute(array $attribute, array &$entry)
+    {
+        $name = $attribute[0];
+        $type = $attribute[1];
+        $value = $attribute[2];
+        if ($type === '::') {
+            $value = base64_decode($value);
+        }
+        if ($name === 'dn') {
+            $entry[$name] = $value;
+        } else if (isset($entry[$name]) && $value !== '') {
+            $entry[$name][] = $value;
+        } else {
+            $entry[$name] = ($value !== '') ? array($value) : array();
+        }
+    }
+
+    /**
+     * Encode $value into a LDIF representation
+     *
+     * @param mixed $value The value to be encoded
+     * @param array $options Additional options used during encoding
+     * @return string The encoded value
+     */
+    public static function encode($value, array $options = array())
+    {
+        $encoder = new self($options);
+        return $encoder->_encode($value);
+    }
+
+    /**
+     * Recursive driver which determines the type of value to be encoded
+     * and then dispatches to the appropriate method.
+     *
+     * @param $value mixed The value to be encoded
+     * @return string Encoded value
+     */
+    protected function _encode($value)
+    {
+        if (is_scalar($value)) {
+            return $this->_encodeString($value);
+        } else if (is_array($value)) {
+            return $this->_encodeAttributes($value);
+        } else if ($value instanceof Zend_Ldap_Node) {
+            return $value->toLdif($this->_options);
+        }
+        return null;
+    }
+
+    /**
+     * Encodes $string according to RFC2849
+     *
+     * @link http://www.faqs.org/rfcs/rfc2849.html
+     *
+     * @param string $string
+     * @param boolen $base64
+     * @return string
+     */
+    protected function _encodeString($string, &$base64 = null)
+    {
+        $string = (string)$string;
+        if (empty($string)) {
+            return '';
+        }
+
+        /*
+         * SAFE-INIT-CHAR = %x01-09 / %x0B-0C / %x0E-1F /
+         *                  %x21-39 / %x3B / %x3D-7F
+         *                ; any value <= 127 except NUL, LF, CR,
+         *                ; SPACE, colon (":", ASCII 58 decimal)
+         *                ; and less-than ("<" , ASCII 60 decimal)
+         *
+         */
+        $unsafe_init_char = array(0, 10, 13, 32, 58, 60);
+        /*
+         * SAFE-CHAR      = %x01-09 / %x0B-0C / %x0E-7F
+         *                ; any value <= 127 decimal except NUL, LF,
+         *                ; and CR
+         */
+        $unsafe_char      = array(0, 10, 13);
+
+        $base64 = false;
+        for ($i = 0; $i < strlen($string); $i++) {
+            $char = ord(substr($string, $i, 1));
+            if ($char >= 127) {
+                $base64 = true;
+                break;
+            } else if ($i === 0 && in_array($char, $unsafe_init_char)) {
+                $base64 = true;
+                break;
+            } else if (in_array($char, $unsafe_char)) {
+                $base64 = true;
+                break;
+            }
+        }
+        // Test for ending space
+        if (substr($string, -1) == ' ') {
+            $base64 = true;
+        }
+
+        if ($base64 === true) {
+            $string = base64_encode($string);
+        }
+
+        return $string;
+    }
+
+    /**
+     * Encodes an attribute with $name and $value according to RFC2849
+     *
+     * @link http://www.faqs.org/rfcs/rfc2849.html
+     *
+     * @param string $name
+     * @param array|string $value
+     * @return string
+     */
+    protected function _encodeAttribute($name, $value)
+    {
+        if (!is_array($value)) {
+            $value = array($value);
+        }
+
+        $output = '';
+
+        if (count($value) < 1) {
+            return $name . ': ';
+        }
+
+        foreach ($value as $v) {
+            $base64 = null;
+            $v = $this->_encodeString($v, $base64);
+            $attribute = $name . ':';
+            if ($base64 === true) {
+                $attribute .= ': ' . $v;
+            } else {
+                $attribute .= ' ' . $v;
+            }
+            if (isset($this->_options['wrap']) && strlen($attribute) > $this->_options['wrap']) {
+                $attribute = trim(chunk_split($attribute, $this->_options['wrap'], PHP_EOL . ' '));
+            }
+            $output .= $attribute . PHP_EOL;
+        }
+        return trim($output, PHP_EOL);
+    }
+
+    /**
+     * Encodes a collection of attributes according to RFC2849
+     *
+     * @link http://www.faqs.org/rfcs/rfc2849.html
+     *
+     * @param array $attributes
+     * @return string
+     */
+    protected function _encodeAttributes(array $attributes)
+    {
+        $string = '';
+        $attributes = array_change_key_case($attributes, CASE_LOWER);
+        if (!$this->_versionWritten && array_key_exists('dn', $attributes) && isset($this->_options['version'])
+                && array_key_exists('objectclass', $attributes)) {
+            $string .= sprintf('version: %d', $this->_options['version']) . PHP_EOL;
+            $this->_versionWritten = true;
+        }
+
+        if (isset($this->_options['sort']) && $this->_options['sort'] === true) {
+            ksort($attributes, SORT_STRING);
+            if (array_key_exists('objectclass', $attributes)) {
+                $oc = $attributes['objectclass'];
+                unset($attributes['objectclass']);
+                $attributes = array_merge(array('objectclass' => $oc), $attributes);
+            }
+            if (array_key_exists('dn', $attributes)) {
+                $dn = $attributes['dn'];
+                unset($attributes['dn']);
+                $attributes = array_merge(array('dn' => $dn), $attributes);
+            }
+        }
+        foreach ($attributes as $key => $value) {
+            $string .= $this->_encodeAttribute($key, $value) . PHP_EOL;
+        }
+        return trim($string, PHP_EOL);
+    }
+}

+ 1098 - 0
library/Zend/Ldap/Node.php

@@ -0,0 +1,1098 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Node
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Ldap
+ */
+require_once 'Zend/Ldap.php';
+/**
+ * @see Zend_Ldap_Node_Abstract
+ */
+require_once 'Zend/Ldap/Node/Abstract.php';
+
+/**
+ * Zend_Ldap_Node provides an object oriented view into a LDAP node.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Node
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Node extends Zend_Ldap_Node_Abstract implements Iterator, RecursiveIterator
+{
+    /**
+     * Holds the node's new DN if node is renamed.
+     *
+     * @var Zend_Ldap_Dn
+     */
+    protected $_newDn;
+    /**
+     * Holds the node's orginal attributes (as loaded).
+     *
+     * @var array
+     */
+    protected $_originalData;
+    /**
+     * This node will be added
+     *
+     * @var boolean
+     */
+    protected $_new;
+    /**
+     * This node will be deleted
+     *
+     * @var boolean
+     */
+    protected $_delete;
+    /**
+     * Holds the connection to the LDAP server if in connected mode.
+     *
+     * @var Zend_Ldap
+     */
+    protected $_ldap;
+
+    /**
+     * Holds an array of the current node's children.
+     *
+     * @var array
+     */
+    protected $_children;
+
+    /**
+     * Controls iteration status
+     *
+     * @var boolean
+     */
+    private $_iteratorRewind = false;
+
+    /**
+     * Constructor.
+     *
+     * Constructor is protected to enforce the use of factory methods.
+     *
+     * @param  Zend_Ldap_Dn $dn
+     * @param  array $data
+     * @param  boolean $fromDataSource
+     * @param  Zend_Ldap $ldap
+     * @throws Zend_Ldap_Exception
+     */
+    protected function __construct(Zend_Ldap_Dn $dn, array $data, $fromDataSource, Zend_Ldap $ldap = null)
+    {
+        parent::__construct($dn, $data, $fromDataSource);
+        if (!is_null($ldap)) $this->attachLdap($ldap);
+        else $this->detachLdap();
+    }
+
+    /**
+     * Serialization callback
+     *
+     * Only DN and attributes will be serialized.
+     *
+     * @return array
+     */
+    public function __sleep()
+    {
+        return array('_dn', '_currentData', '_newDn', '_originalData',
+            '_new', '_delete', '_children');
+    }
+
+    /**
+     * Deserialization callback
+     *
+     * Enforces a detached node.
+     *
+     * @return null
+     */
+    public function __wakeup()
+    {
+        $this->detachLdap();
+    }
+
+    /**
+     * Gets the current LDAP connection.
+     *
+     * @return Zend_Ldap
+     * @throws Zend_Ldap_Exception
+     */
+    public function getLdap()
+    {
+        if (is_null($this->_ldap)) {
+            /**
+             * @see Zend_Ldap_Exception
+             */
+            require_once 'Zend/Ldap/Exception.php';
+            throw new Zend_Ldap_Exception(null, 'No LDAP connection specified.', Zend_Ldap_Exception::LDAP_OTHER);
+        }
+        else return $this->_ldap;
+    }
+
+    /**
+     * Attach node to an LDAP connection
+     *
+     * This is an offline method.
+     *
+     * @uses   Zend_Ldap_Dn::isChildOf()
+     * @param  Zend_Ldap $ldap
+     * @return Zend_Ldap_Node *Provides a fluid interface*
+     * @throws Zend_Ldap_Exception
+     */
+    public function attachLdap(Zend_Ldap $ldap)
+    {
+        if (!Zend_Ldap_Dn::isChildOf($this->_getDn(), $ldap->getBaseDn())) {
+            /**
+             * @see Zend_Ldap_Exception
+             */
+            require_once 'Zend/Ldap/Exception.php';
+            throw new Zend_Ldap_Exception(null, 'LDAP connection is not responsible for given node.',
+                Zend_Ldap_Exception::LDAP_OTHER);
+        }
+
+        if ($ldap !== $this->_ldap) {
+            $this->_ldap = $ldap;
+            if (is_array($this->_children)) {
+                foreach ($this->_children as $child) {
+                    $child->attachLdap($ldap);
+                }
+            }
+        }
+        return $this;
+    }
+
+    /**
+     * Detach node from LDAP connection
+     *
+     * This is an offline method.
+     *
+     * @return Zend_Ldap_Node *Provides a fluid interface*
+     */
+    public function detachLdap()
+    {
+        $this->_ldap = null;
+        if (is_array($this->_children)) {
+            foreach ($this->_children as $child) {
+                $child->detachLdap();
+            }
+        }
+        return $this;
+    }
+
+    /**
+     * Checks if the current node is attached to a LDAP server.
+     *
+     * This is an offline method.
+     *
+     * @return boolean
+     */
+    public function isAttached()
+    {
+        return (!is_null($this->_ldap));
+    }
+
+    /**
+     * @param  array $data
+     * @param  boolean $fromDataSource
+     * @throws Zend_Ldap_Exception
+     */
+    protected function _loadData(array $data, $fromDataSource)
+    {
+        parent::_loadData($data, $fromDataSource);
+        if ($fromDataSource === true) {
+            $this->_originalData = $data;
+        } else {
+            $this->_originalData = array();
+        }
+        $this->_children = null;
+        $this->_markAsNew(($fromDataSource === true) ? false : true);
+        $this->_markAsToBeDeleted(false);
+    }
+
+    /**
+     * Factory method to create a new detached Zend_Ldap_Node for a given DN.
+     *
+     * @param  string|array|Zend_Ldap_Dn $dn
+     * @param  array $objectClass
+     * @return Zend_Ldap_Node
+     * @throws Zend_Ldap_Exception
+     */
+    public static function create($dn, array $objectClass = array())
+    {
+        if (is_string($dn) || is_array($dn)) {
+            $dn = Zend_Ldap_Dn::factory($dn);
+        } else if ($dn instanceof Zend_Ldap_Dn) {
+            $dn = clone $dn;
+        } else {
+            /**
+             * @see Zend_Ldap_Exception
+             */
+            require_once 'Zend/Ldap/Exception.php';
+            throw new Zend_Ldap_Exception(null, '$dn is of a wrong data type.');
+        }
+        $new = new self($dn, array(), false, null);
+        $new->_ensureRdnAttributeValues();
+        $new->setAttribute('objectClass', $objectClass);
+        return $new;
+    }
+
+    /**
+     * Factory method to create an attached Zend_Ldap_Node for a given DN.
+     *
+     * @param  string|array|Zend_Ldap_Dn $dn
+     * @param  Zend_Ldap $ldap
+     * @return Zend_Ldap_Node
+     * @throws Zend_Ldap_Exception
+     */
+    public static function fromLdap($dn, Zend_Ldap $ldap)
+    {
+        if (is_string($dn) || is_array($dn)) {
+            $dn = Zend_Ldap_Dn::factory($dn);
+        } else if ($dn instanceof Zend_Ldap_Dn) {
+            $dn = clone $dn;
+        } else {
+            /**
+             * @see Zend_Ldap_Exception
+             */
+            require_once 'Zend/Ldap/Exception.php';
+            throw new Zend_Ldap_Exception(null, '$dn is of a wrong data type.');
+        }
+        $data = $ldap->getEntry($dn, array('*', '+'), true);
+        $entry = new self($dn, $data, true, $ldap);
+        return $entry;
+    }
+
+    /**
+     * Factory method to create a detached Zend_Ldap_Node from array data.
+     *
+     * @param  array $data
+     * @param  boolean $fromDataSource
+     * @return Zend_Ldap_Node
+     * @throws Zend_Ldap_Exception
+     */
+    public static function fromArray(array $data, $fromDataSource = false)
+    {
+        if (!array_key_exists('dn', $data)) {
+            /**
+             * @see Zend_Ldap_Exception
+             */
+            require_once 'Zend/Ldap/Exception.php';
+            throw new Zend_Ldap_Exception(null, '\'dn\' key is missing in array.');
+        }
+        if (is_string($data['dn']) || is_array($data['dn'])) {
+            $dn = Zend_Ldap_Dn::factory($data['dn']);
+        } else if ($data['dn'] instanceof Zend_Ldap_Dn) {
+            $dn = clone $data['dn'];
+        } else {
+            /**
+             * @see Zend_Ldap_Exception
+             */
+            require_once 'Zend/Ldap/Exception.php';
+            throw new Zend_Ldap_Exception(null, '\'dn\' key is of a wrong data type.');
+        }
+        $fromDataSource = ($fromDataSource === true) ? true : false;
+        $new = new self($dn, $data, $fromDataSource, null);
+        $new->_ensureRdnAttributeValues();
+        return $new;
+    }
+
+    /**
+     * Ensures that teh RDN attributes are correctly set.
+     *
+     * @return void
+     */
+    protected function _ensureRdnAttributeValues()
+    {
+        foreach ($this->getRdnArray() as $key => $value) {
+            Zend_Ldap_Attribute::setAttribute($this->_currentData, $key, $value, false);
+        }
+    }
+
+    /**
+     * Marks this node as new.
+     *
+     * Node will be added (instead of updated) on calling update() if $new is true.
+     *
+     * @param boolean $new
+     */
+    protected function _markAsNew($new)
+    {
+        $this->_new = ($new === false) ? false : true;
+    }
+
+    /**
+     * Tells if the node is consiedered as new (not present on the server)
+     *
+     * Please note, that this doesn't tell you if the node is present on the server.
+     * Use {@link exits()} to see if a node is already there.
+     *
+     * @return boolean
+     */
+    public function isNew()
+    {
+        return $this->_new;
+    }
+
+    /**
+     * Marks this node as to be deleted.
+     *
+     * Node will be deleted on calling update() if $delete is true.
+     *
+     * @param boolean $delete
+     */
+    protected function _markAsToBeDeleted($delete)
+    {
+        $this->_delete = ($delete === true) ? true : false;
+    }
+
+
+    /**
+    * Is this node going to be deleted once update() is called?
+    *
+    * @return boolean
+    */
+    public function willBeDeleted()
+    {
+        return $this->_delete;
+    }
+
+    /**
+     * Marks this node as to be deleted
+     *
+     * Node will be deleted on calling update() if $delete is true.
+     *
+     * @return Zend_Ldap_Node *Provides a fluid interface*
+     */
+    public function delete()
+    {
+        $this->_markAsToBeDeleted(true);
+        return $this;
+    }
+
+    /**
+    * Is this node going to be moved once update() is called?
+    *
+    * @return boolean
+    */
+    public function willBeMoved()
+    {
+        if ($this->isNew() || $this->willBeDeleted()) {
+            return false;
+        } else if ($this->_newDn !== null) {
+            return ($this->_dn != $this->_newDn);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Sends all pending changes to the LDAP server
+     *
+     * @param Zend_Ldap $ldap
+     * @return Zend_Ldap_Node *Provides a fluid interface*
+     * @throws Zend_Ldap_Exception
+     */
+    public function update(Zend_Ldap $ldap = null)
+    {
+        if ($ldap !== null) {
+            $this->attachLdap($ldap);
+        }
+        $ldap = $this->getLdap();
+        if (!($ldap instanceof Zend_Ldap)) {
+            /**
+             * @see Zend_Ldap_Exception
+             */
+            require_once 'Zend/Ldap/Exception.php';
+            throw new Zend_Ldap_Exception(null, 'No LDAP connection available');
+        }
+
+        if ($this->willBeDeleted()) {
+            if ($ldap->exists($this->_dn)) {
+                $ldap->delete($this->_dn);
+            }
+            return $this;
+        }
+
+        if ($this->isNew()) {
+            $data = $this->getData();
+            $ldap->add($this->_getDn(), $data);
+            $this->_loadData($data, true);
+            return $this;
+        }
+
+        $changedData = $this->getChangedData();
+        if ($this->willBeMoved()) {
+            $recursive = $this->hasChildren();
+            $ldap->rename($this->_dn, $this->_newDn, $recursive, false);
+            foreach ($this->_newDn->getRdn() as $key => $value) {
+                if (array_key_exists($key, $changedData)) {
+                    unset($changedData[$key]);
+                }
+            }
+            $this->_dn = $this->_newDn;
+            $this->_newDn = null;
+        }
+        if (count($changedData) > 0) {
+            $ldap->update($this->_getDn(), $changedData);
+        }
+        $this->_originalData = $this->_currentData;
+        return $this;
+    }
+
+    /**
+     * Gets the DN of the current node as a Zend_Ldap_Dn.
+     *
+     * This is an offline method.
+     *
+     * @return Zend_Ldap_Dn
+     */
+    protected function _getDn()
+    {
+        return ($this->_newDn === null) ? parent::_getDn() : $this->_newDn;
+    }
+
+    /**
+     * Gets the current DN of the current node as a Zend_Ldap_Dn.
+     * The method returns a clone of the node's DN to prohibit modification.
+     *
+     * This is an offline method.
+     *
+     * @return Zend_Ldap_Dn
+     */
+    public function getCurrentDn()
+    {
+        $dn = clone parent::_getDn();
+        return $dn;
+    }
+
+    /**
+     * Sets the new DN for this node
+     *
+     * This is an offline method.
+     *
+     * @param Zend_Ldap_Dn|string|array $newDn
+     * @throws Zend_Ldap_Exception
+     * @return Zend_Ldap_Node *Provides a fluid interface*
+     */
+    public function setDn($newDn)
+    {
+        if ($newDn instanceof Zend_Ldap_Dn) {
+            $this->_newDn = clone $newDn;
+        } else {
+            $this->_newDn = Zend_Ldap_Dn::factory($newDn);
+        }
+        $this->_ensureRdnAttributeValues();
+        return $this;
+    }
+
+    /**
+     * {@see setDn()}
+     *
+     * This is an offline method.
+     *
+     * @param Zend_Ldap_Dn|string|array $newDn
+     * @throws Zend_Ldap_Exception
+     * @return Zend_Ldap_Node *Provides a fluid interface*
+     */
+    public function move($newDn)
+    {
+        return $this->setDn($newDn);
+    }
+
+    /**
+     * {@see setDn()}
+     *
+     * This is an offline method.
+     *
+     * @param Zend_Ldap_Dn|string|array $newDn
+     * @throws Zend_Ldap_Exception
+     * @return Zend_Ldap_Node *Provides a fluid interface*
+     */
+    public function rename($newDn)
+    {
+        return $this->setDn($newDn);
+    }
+
+    /**
+     * Sets the objectClass.
+     *
+     * This is an offline method.
+     *
+     * @param  array|string $value
+     * @return Zend_Ldap_Node *Provides a fluid interface*
+     * @throws Zend_Ldap_Exception
+     */
+    public function setObjectClass($value)
+    {
+        $this->setAttribute('objectClass', $value);
+        return $this;
+    }
+
+    /**
+     * Appends to the objectClass.
+     *
+     * This is an offline method.
+     *
+     * @param  array|string $value
+     * @return Zend_Ldap_Node *Provides a fluid interface*
+     * @throws Zend_Ldap_Exception
+     */
+    public function appendObjectClass($value)
+    {
+        $this->appendToAttribute('objectClass', $value);
+        return $this;
+    }
+
+    /**
+     * Returns a LDIF representation of the current node
+     *
+     * @param array $options Additional options used during encoding
+     * @return string
+     */
+    public function toLdif(array $options = array())
+    {
+        $attributes = array_merge(array('dn' => $this->getDnString()), $this->getData(false));
+        /**
+         * Zend_Ldap_Ldif_Encoder
+         */
+        require_once 'Zend/Ldap/Ldif/Encoder.php';
+        return Zend_Ldap_Ldif_Encoder::encode($attributes, $options);
+    }
+
+    /**
+     * Gets changed node data.
+     *
+     * The array contains all changed attributes.
+     * This format can be used in {@link Zend_Ldap::add()} and {@link Zend_Ldap::update()}.
+     *
+     * This is an offline method.
+     *
+     * @return array
+     */
+    public function getChangedData()
+    {
+        $changed = array();
+        foreach ($this->_currentData as $key => $value) {
+            if (!array_key_exists($key, $this->_originalData) && !empty($value)) {
+                $changed[$key] = $value;
+            } else if ($this->_originalData[$key] !== $this->_currentData[$key]) {
+                $changed[$key] = $value;
+            }
+        }
+        return $changed;
+    }
+
+    /**
+     * Returns all changes made.
+     *
+     * This is an offline method.
+     *
+     * @return array
+     */
+    public function getChanges()
+    {
+        $changes = array(
+            'add'     => array(),
+            'delete'  => array(),
+            'replace' => array());
+        foreach ($this->_currentData as $key => $value) {
+            if (!array_key_exists($key, $this->_originalData) && !empty($value)) {
+                $changes['add'][$key] = $value;
+            } else if (count($this->_originalData[$key]) === 0 && !empty($value)) {
+                $changes['add'][$key] = $value;
+            } else if ($this->_originalData[$key] !== $this->_currentData[$key]) {
+                if (empty($value)) {
+                    $changes['delete'][$key] = $value;
+                } else {
+                    $changes['replace'][$key] = $value;
+                }
+            }
+        }
+        return $changes;
+    }
+
+    /**
+     * Sets a LDAP attribute.
+     *
+     * This is an offline method.
+     *
+     * @param  string $name
+     * @param  mixed $value
+     * @return Zend_Ldap_Node *Provides a fluid interface*
+     * @throws Zend_Ldap_Exception
+     */
+    public function setAttribute($name, $value)
+    {
+        $this->_setAttribute($name, $value, false);
+        return $this;
+    }
+
+    /**
+     * Appends to a LDAP attribute.
+     *
+     * This is an offline method.
+     *
+     * @param  string $name
+     * @param  mixed $value
+     * @return Zend_Ldap_Node *Provides a fluid interface*
+     * @throws Zend_Ldap_Exception
+     */
+    public function appendToAttribute($name, $value)
+    {
+        $this->_setAttribute($name, $value, true);
+        return $this;
+    }
+
+    /**
+     * Checks if the attribute can be set and sets it accordingly.
+     *
+     * @param  string $name
+     * @param  mixed $value
+     * @param  boolean $append
+     * @throws Zend_Ldap_Exception
+     */
+    protected function _setAttribute($name, $value, $append)
+    {
+        $this->_assertChangeableAttribute($name);
+        Zend_Ldap_Attribute::setAttribute($this->_currentData, $name, $value, $append);
+    }
+
+    /**
+     * Sets a LDAP date/time attribute.
+     *
+     * This is an offline method.
+     *
+     * @param  string $name
+     * @param  integer|array $value
+     * @param  boolean $utc
+     * @return Zend_Ldap_Node *Provides a fluid interface*
+     * @throws Zend_Ldap_Exception
+     */
+    public function setDateTimeAttribute($name, $value, $utc = false)
+    {
+        $this->_setDateTimeAttribute($name, $value, $utc, false);
+        return $this;
+    }
+
+    /**
+     * Appends to a LDAP date/time attribute.
+     *
+     * This is an offline method.
+     *
+     * @param  string $name
+     * @param  integer|array $value
+     * @param  boolean $utc
+     * @return Zend_Ldap_Node *Provides a fluid interface*
+     * @throws Zend_Ldap_Exception
+     */
+    public function appendToDateTimeAttribute($name, $value, $utc = false)
+    {
+        $this->_setDateTimeAttribute($name, $value, $utc, true);
+        return $this;
+    }
+
+    /**
+     * Checks if the attribute can be set and sets it accordingly.
+     *
+     * @param  string $name
+     * @param  integer|array $value
+     * @param  boolean $utc
+     * @param  boolean $append
+     * @throws Zend_Ldap_Exception
+     */
+    protected function _setDateTimeAttribute($name, $value, $utc, $append)
+    {
+        $this->_assertChangeableAttribute($name);
+        Zend_Ldap_Attribute::setDateTimeAttribute($this->_currentData, $name, $value, $utc, $append);
+    }
+
+    /**
+     * Sets a LDAP password.
+     *
+     * @param  string $password
+     * @param  string $hashType
+     * @param  string $attribName
+     * @return Zend_Ldap_Node *Provides a fluid interface*
+     * @throws Zend_Ldap_Exception
+     */
+    public function setPasswordAttribute($password, $hashType = Zend_Ldap_Attribute::PASSWORD_HASH_MD5,
+        $attribName = 'userPassword')
+    {
+        $this->_assertChangeableAttribute($attribName);
+        Zend_Ldap_Attribute::setPassword($this->_currentData, $password, $hashType, $attribName);
+        return $this;
+    }
+
+    /**
+     * Deletes a LDAP attribute.
+     *
+     * This method deletes the attribute.
+     *
+     * This is an offline method.
+     *
+     * @param  string $name
+     * @return Zend_Ldap_Node *Provides a fluid interface*
+     * @throws Zend_Ldap_Exception
+     */
+    public function deleteAttribute($name)
+    {
+        if ($this->existsAttribute($name, true)) {
+            $this->_setAttribute($name, null, false);
+        }
+        return $this;
+    }
+
+    /**
+     * Removes duplicate values from a LDAP attribute
+     *
+     * @param string $attribName
+     * @return void
+     */
+    public function removeDuplicatesFromAttribute($attribName)
+    {
+        Zend_Ldap_Attribute::removeDuplicatesFromAttribute($this->_currentData, $attribName);
+    }
+
+    /**
+     * Remove given values from a LDAP attribute
+     *
+     * @param string $attribName
+     * @param mixed|array $value
+     * @return void
+     */
+    public function removeFromAttribute($attribName, $value)
+    {
+        Zend_Ldap_Attribute::removeFromAttribute($this->_currentData, $attribName, $value);
+    }
+
+    /**
+     * @param  string $name
+     * @return boolean
+     * @throws Zend_Ldap_Exception
+     */
+    protected function _assertChangeableAttribute($name)
+    {
+        $name = strtolower($name);
+        $rdn = $this->getRdnArray(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER);
+        if ($name == 'dn') {
+            /**
+             * @see Zend_Ldap_Exception
+             */
+            require_once 'Zend/Ldap/Exception.php';
+            throw new Zend_Ldap_Exception(null, 'DN cannot be changed.');
+        }
+        else if (array_key_exists($name, $rdn)) {
+            /**
+             * @see Zend_Ldap_Exception
+             */
+            require_once 'Zend/Ldap/Exception.php';
+            throw new Zend_Ldap_Exception(null, 'Cannot change attribute because it\'s part of the RDN');
+        } else if (in_array($name, self::$_systemAttributes)) {
+            /**
+             * @see Zend_Ldap_Exception
+             */
+            require_once 'Zend/Ldap/Exception.php';
+            throw new Zend_Ldap_Exception(null, 'Cannot change attribute because it\'s read-only');
+        }
+        else return true;
+    }
+
+    /**
+     * Sets a LDAP attribute.
+     *
+     * This is an offline method.
+     *
+     * @param  string $name
+     * @param  mixed $value
+     * @return null
+     * @throws Zend_Ldap_Exception
+     */
+    public function __set($name, $value)
+    {
+        $this->setAttribute($name, $value);
+    }
+
+    /**
+     * Deletes a LDAP attribute.
+     *
+     * This method deletes the attribute.
+     *
+     * This is an offline method.
+     *
+     * @param  string $name
+     * @return null
+     * @throws Zend_Ldap_Exception
+     */
+    public function __unset($name)
+    {
+        $this->deleteAttribute($name);
+    }
+
+    /**
+     * Sets a LDAP attribute.
+     * Implements ArrayAccess.
+     *
+     * This is an offline method.
+     *
+     * @param  string $name
+     * @param  mixed $value
+     * @return null
+     * @throws Zend_Ldap_Exception
+     */
+    public function offsetSet($name, $value)
+    {
+        $this->setAttribute($name, $value);
+    }
+
+    /**
+     * Deletes a LDAP attribute.
+     * Implements ArrayAccess.
+     *
+     * This method deletes the attribute.
+     *
+     * This is an offline method.
+     *
+     * @param  string $name
+     * @return null
+     * @throws Zend_Ldap_Exception
+     */
+    public function offsetUnset($name)
+    {
+        $this->deleteAttribute($name);
+    }
+
+    /**
+     * Check if node exists on LDAP.
+     *
+     * This is an online method.
+     *
+     * @param Zend_Ldap $ldap
+     * @return boolean
+     * @throws Zend_Ldap_Exception
+     */
+    public function exists(Zend_Ldap $ldap = null)
+    {
+        if ($ldap !== null) {
+            $this->attachLdap($ldap);
+        }
+        $ldap = $this->getLdap();
+        return $ldap->exists($this->_getDn());
+    }
+
+    /**
+     * Reload node attributes from LDAP.
+     *
+     * This is an online method.
+     *
+     * @param  Zend_Ldap $ldap
+     * @return Zend_Ldap_Node *Provides a fluid interface*
+     * @throws Zend_Ldap_Exception
+     */
+    public function reload(Zend_Ldap $ldap = null)
+    {
+        if ($ldap !== null) {
+            $this->attachLdap($ldap);
+        }
+        $ldap = $this->getLdap();
+        parent::reload($ldap);
+        return $this;
+    }
+
+    /**
+     * Search current subtree with given options.
+     *
+     * This is an online method.
+     *
+     * @param string|Zend_Ldap_Filter_Abstract $filter
+     * @param integer $scope
+     * @param string $sort
+     * @return Zend_Ldap_Node_Collection
+     * @throws Zend_Ldap_Exception
+     */
+    public function searchSubtree($filter, $scope = Zend_Ldap::SEARCH_SCOPE_SUB, $sort = null)
+    {
+        /**
+         * @see Zend_Ldap_Node_Collection
+         */
+        require_once 'Zend/Ldap/Node/Collection.php';
+        return $this->getLdap()->search($filter, $this->_getDn(), $scope, array('*', '+'), $sort,
+            'Zend_Ldap_Node_Collection');
+    }
+
+    /**
+     * Count items in current subtree found by given filter.
+     *
+     * This is an online method.
+     *
+     * @param string|Zend_Ldap_Filter_Abstract $filter
+     * @param integer $scope
+     * @return integer
+     * @throws Zend_Ldap_Exception
+     */
+    public function countSubtree($filter, $scope = Zend_Ldap::SEARCH_SCOPE_SUB)
+    {
+        return $this->getLdap()->count($filter, $this->_getDn(), $scope);
+    }
+
+    /**
+     * Count children of current node.
+     *
+     * This is an online method.
+     *
+     * @return integer
+     * @throws Zend_Ldap_Exception
+     */
+    public function countChildren()
+    {
+        return $this->countSubtree('(objectClass=*)', Zend_Ldap::SEARCH_SCOPE_ONE);
+    }
+
+    /**
+     * Gets children of current node.
+     *
+     * This is an online method.
+     *
+     * @param  string|Zend_Ldap_Filter_Abstract $filter
+     * @param  string $sort
+     * @return Zend_Ldap_Node_Collection
+     * @throws Zend_Ldap_Exception
+     */
+    public function searchChildren($filter, $sort = null)
+    {
+        return $this->searchSubtree($filter, Zend_Ldap::SEARCH_SCOPE_ONE, $sort);
+    }
+
+    /**
+     * Checks if current node has children.
+     * Returns whether the current element has children.
+     *
+     * Can be used offline but returns false if children have not been retrieved yet.
+     *
+     * @return boolean
+     * @throws Zend_Ldap_Exception
+     */
+    public function hasChildren()
+    {
+        if (!is_array($this->_children)) {
+            if ($this->isAttached()) {
+                return ($this->countChildren() > 0);
+            } else {
+                return false;
+            }
+        } else {
+            return (count($this->_children) > 0);
+        }
+    }
+
+    /**
+     * Returns the children for the current node.
+     *
+     * Can be used offline but returns an empty array if children have not been retrieved yet.
+     *
+     * @return Zend_Ldap_Node_ChildrenIterator
+     * @throws Zend_Ldap_Exception
+     */
+    public function getChildren()
+    {
+        if (!is_array($this->_children)) {
+            $this->_children = array();
+            if ($this->isAttached()) {
+                $children = $this->searchChildren('(objectClass=*)', null);
+                foreach ($children as $child) {
+                    $this->_children[$child->getRdnString(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER)] = $child;
+                }
+            }
+        }
+        /**
+         * @see Zend_Ldap_Node_ChildrenIterator
+         */
+        require_once 'Zend/Ldap/Node/ChildrenIterator.php';
+        return new Zend_Ldap_Node_ChildrenIterator($this->_children);
+    }
+
+    /**
+     * Returns the parent of the current node.
+     *
+     * @param  Zend_Ldap $ldap
+     * @return Zend_Ldap_Node
+     * @throws Zend_Ldap_Exception
+     */
+    public function getParent(Zend_Ldap $ldap = null)
+    {
+        if ($ldap !== null) {
+            $this->attachLdap($ldap);
+        }
+        $ldap = $this->getLdap();
+        $parentDn = $this->_getDn()->getParentDn(1);
+        return self::fromLdap($parentDn, $ldap);
+    }
+
+    /**
+     * Return the current attribute.
+     * Implements Iterator
+     *
+     * @return array
+     */
+    public function current()
+    {
+        return $this;
+    }
+
+    /**
+     * Return the attribute name.
+     * Implements Iterator
+     *
+     * @return string
+     */
+    public function key()
+    {
+        return $this->getRdnString();
+    }
+
+    /**
+     * Move forward to next attribute.
+     * Implements Iterator
+     */
+    public function next()
+    {
+        $this->_iteratorRewind = false;
+    }
+
+    /**
+     * Rewind the Iterator to the first attribute.
+     * Implements Iterator
+     */
+    public function rewind()
+    {
+        $this->_iteratorRewind = true;
+    }
+
+    /**
+     * Check if there is a current attribute
+     * after calls to rewind() or next().
+     * Implements Iterator
+     *
+     * @return boolean
+     */
+    public function valid()
+    {
+        return $this->_iteratorRewind;
+    }
+}

+ 485 - 0
library/Zend/Ldap/Node/Abstract.php

@@ -0,0 +1,485 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Node
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Ldap_Attribute
+ */
+require_once 'Zend/Ldap/Attribute.php';
+/**
+ * @see Zend_Ldap_Dn
+ */
+require_once 'Zend/Ldap/Dn.php';
+
+/**
+ * Zend_Ldap_Node_Abstract provides a bas eimplementation for LDAP nodes
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Node
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+abstract class Zend_Ldap_Node_Abstract implements ArrayAccess, Countable
+{
+    protected static $_systemAttributes=array('createtimestamp', 'creatorsname',
+        'entrycsn', 'entrydn', 'entryuuid', 'hassubordinates', 'modifiersname',
+        'modifytimestamp', 'structuralobjectclass', 'subschemasubentry',
+        'distinguishedname', 'instancetype', 'name', 'objectcategory', 'objectguid',
+        'usnchanged', 'usncreated', 'whenchanged', 'whencreated');
+
+    /**
+     * Holds the node's DN.
+     *
+     * @var Zend_Ldap_Dn
+     */
+    protected $_dn;
+
+    /**
+     * Holds the node's current data.
+     *
+     * @var array
+     */
+    protected $_currentData;
+
+    /**
+     * Constructor.
+     *
+     * Constructor is protected to enforce the use of factory methods.
+     *
+     * @param  Zend_Ldap_Dn $dn
+     * @param  array $data
+     * @param  boolean $fromDataSource
+     */
+    protected function __construct(Zend_Ldap_Dn $dn, array $data, $fromDataSource)
+    {
+        $this->_dn = $dn;
+        $this->_loadData($data, $fromDataSource);
+    }
+
+    /**
+     * @param  array $data
+     * @param  boolean $fromDataSource
+     * @throws Zend_Ldap_Exception
+     */
+    protected function _loadData(array $data, $fromDataSource)
+    {
+        if (array_key_exists('dn', $data)) {
+            unset($data['dn']);
+        }
+        ksort($data, SORT_STRING);
+        $this->_currentData = $data;
+    }
+
+    /**
+     * Reload node attributes from LDAP.
+     *
+     * This is an online method.
+     *
+     * @param  Zend_Ldap $ldap
+     * @return Zend_Ldap_Node_Abstract *Provides a fluid interface*
+     * @throws Zend_Ldap_Exception
+     */
+    public function reload(Zend_Ldap $ldap = null)
+    {
+        if ($ldap !== null) {
+            $data = $ldap->getEntry($this->_getDn(), array('*', '+'), true);
+            $this->_loadData($data, true);
+        }
+        return $this;
+    }
+
+    /**
+     * Gets the DN of the current node as a Zend_Ldap_Dn.
+     *
+     * This is an offline method.
+     *
+     * @return Zend_Ldap_Dn
+     */
+    protected function _getDn()
+    {
+        return $this->_dn;
+    }
+
+    /**
+     * Gets the DN of the current node as a Zend_Ldap_Dn.
+     * The method returns a clone of the node's DN to prohibit modification.
+     *
+     * This is an offline method.
+     *
+     * @return Zend_Ldap_Dn
+     */
+    public function getDn()
+    {
+        $dn = clone $this->_getDn();
+        return $dn;
+    }
+
+    /**
+     * Gets the DN of the current node as a string.
+     *
+     * This is an offline method.
+     *
+     * @param  string $caseFold
+     * @return string
+     */
+    public function getDnString($caseFold = null)
+    {
+        return $this->_getDn()->toString($caseFold);
+    }
+
+    /**
+     * Gets the DN of the current node as an array.
+     *
+     * This is an offline method.
+     *
+     * @param  string $caseFold
+     * @return array
+     */
+    public function getDnArray($caseFold = null)
+    {
+        return $this->_getDn()->toArray($caseFold);
+    }
+
+    /**
+     * Gets the RDN of the current node as a string.
+     *
+     * This is an offline method.
+     *
+     * @param  string $caseFold
+     * @return string
+     */
+    public function getRdnString($caseFold = null)
+    {
+        return $this->_getDn()->getRdnString($caseFold);
+    }
+
+    /**
+     * Gets the RDN of the current node as an array.
+     *
+     * This is an offline method.
+     *
+     * @param  string $caseFold
+     * @return array
+     */
+    public function getRdnArray($caseFold = null)
+    {
+        return $this->_getDn()->getRdn($caseFold);
+    }
+
+    /**
+     * Gets the objectClass of the node
+     *
+     * @return array
+     */
+    public function getObjectClass()
+    {
+        return $this->getAttribute('objectClass', null);
+    }
+
+    /**
+     * Gets all attributes of node.
+     *
+     * The collection contains all attributes.
+     *
+     * This is an offline method.
+     *
+     * @param boolean $includeSystemAttributes
+     * @return array
+     */
+    public function getAttributes($includeSystemAttributes = true)
+    {
+        $data = array();
+        foreach ($this->getData($includeSystemAttributes) as $name => $value) {
+            $data[$name] = $this->getAttribute($name, null);
+        }
+        return $data;
+    }
+
+    /**
+     * Returns the DN of the current node. {@see getDnString()}
+     *
+     * @return string
+     */
+    public function toString()
+    {
+        return $this->getDnString();
+    }
+
+    /**
+     * Cast to string representation {@see toString()}
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return $this->toString();
+    }
+
+    /**
+     * Returns an array representation of the current node
+     *
+     * @param boolean $includeSystemAttributes
+     * @return array
+     */
+    public function toArray($includeSystemAttributes = true)
+    {
+        $attributes = $this->getAttributes($includeSystemAttributes);
+        return array_merge(array('dn' => $this->getDnString()), $attributes);
+    }
+
+    /**
+     * Returns a JSON representation of the current node
+     *
+     * @param boolean $includeSystemAttributes
+     * @return string
+     */
+    public function toJson($includeSystemAttributes = true)
+    {
+        return json_encode($this->toArray($includeSystemAttributes));
+    }
+
+    /**
+     * Gets node attributes.
+     *
+     * The array contains all attributes in its internal format (no conversion).
+     *
+     * This is an offline method.
+     *
+     * @param boolean $includeSystemAttributes
+     * @return array
+     */
+    public function getData($includeSystemAttributes = true)
+    {
+        if ($includeSystemAttributes === false) {
+            $data = array();
+            foreach ($this->_currentData as $key => $value) {
+                if (!in_array($key, self::$_systemAttributes)) {
+                    $data[$key] = $value;
+                }
+            }
+            return $data;
+        } else {
+            return $this->_currentData;
+        }
+    }
+
+    /**
+     * Checks whether a given attribute exists.
+     *
+     * If $emptyExists is false empty attributes (containing only array()) are
+     * treated as non-existent returning false.
+     * If $emptyExists is true empty attributes are treated as existent returning
+     * true. In this case method returns false only if the attribute name is
+     * missing in the key-collection.
+     *
+     * @param  string $name
+     * @param  boolean $emptyExists
+     * @return boolean
+     */
+    public function existsAttribute($name, $emptyExists = false)
+    {
+        $name = strtolower($name);
+        if (isset($this->_currentData[$name])) {
+            if ($emptyExists) return true;
+            return count($this->_currentData[$name])>0;
+        }
+        else return false;
+    }
+
+    /**
+     * Checks if the given value(s) exist in the attribute
+     *
+     * @param string $attribName
+     * @param mixed|array $value
+     * @return boolean
+     */
+    public function attributeHasValue($attribName, $value)
+    {
+        return Zend_Ldap_Attribute::attributeHasValue($this->_currentData, $attribName, $value);
+    }
+
+    /**
+     * Gets a LDAP attribute.
+     *
+     * This is an offline method.
+     *
+     * @param  string $name
+     * @param  integer $index
+     * @return mixed
+     * @throws Zend_Ldap_Exception
+     */
+    public function getAttribute($name, $index = null)
+    {
+        if ($name == 'dn') {
+            return $this->getDnString();
+        }
+        else {
+            return Zend_Ldap_Attribute::getAttribute($this->_currentData, $name, $index);
+        }
+    }
+
+    /**
+     * Gets a LDAP date/time attribute.
+     *
+     * This is an offline method.
+     *
+     * @param  string $name
+     * @param  integer $index
+     * @return array|integer
+     * @throws Zend_Ldap_Exception
+     */
+    public function getDateTimeAttribute($name, $index = null)
+    {
+        return Zend_Ldap_Attribute::getDateTimeAttribute($this->_currentData, $name, $index);
+    }
+
+    /**
+     * Sets a LDAP attribute.
+     *
+     * This is an offline method.
+     *
+     * @param  string $name
+     * @param  mixed $value
+     * @return null
+     * @throws BadMethodCallException
+     */
+    public function __set($name, $value)
+    {
+        throw new BadMethodCallException();
+    }
+
+    /**
+     * Gets a LDAP attribute.
+     *
+     * This is an offline method.
+     *
+     * @param  string $name
+     * @return array
+     * @throws Zend_Ldap_Exception
+     */
+    public function __get($name)
+    {
+        return $this->getAttribute($name, null);
+    }
+
+    /**
+     * Deletes a LDAP attribute.
+     *
+     * This method deletes the attribute.
+     *
+     * This is an offline method.
+     *
+     * @param  string $name
+     * @return null
+     * @throws BadMethodCallException
+     */
+    public function __unset($name)
+    {
+        throw new BadMethodCallException();
+    }
+
+    /**
+     * Checks whether a given attribute exists.
+     *
+     * Empty attributes will be treated as non-existent.
+     *
+     * @param  string $name
+     * @return boolean
+     */
+    public function __isset($name)
+    {
+        return $this->existsAttribute($name, false);
+    }
+
+    /**
+     * Sets a LDAP attribute.
+     * Implements ArrayAccess.
+     *
+     * This is an offline method.
+     *
+     * @param  string $name
+     * @param  mixed $value
+     * @return null
+     * @throws BadMethodCallException
+     */
+    public function offsetSet($name, $value)
+    {
+        throw new BadMethodCallException();
+    }
+
+    /**
+     * Gets a LDAP attribute.
+     * Implements ArrayAccess.
+     *
+     * This is an offline method.
+     *
+     * @param  string $name
+     * @return array
+     * @throws Zend_Ldap_Exception
+     */
+    public function offsetGet($name)
+    {
+        return $this->getAttribute($name, null);
+    }
+
+    /**
+     * Deletes a LDAP attribute.
+     * Implements ArrayAccess.
+     *
+     * This method deletes the attribute.
+     *
+     * This is an offline method.
+     *
+     * @param  string $name
+     * @return null
+     * @throws BadMethodCallException
+     */
+    public function offsetUnset($name)
+    {
+        throw new BadMethodCallException();
+    }
+
+    /**
+     * Checks whether a given attribute exists.
+     * Implements ArrayAccess.
+     *
+     * Empty attributes will be treated as non-existent.
+     *
+     * @param  string $name
+     * @return boolean
+     */
+    public function offsetExists($name)
+    {
+        return $this->existsAttribute($name, false);
+    }
+
+    /**
+     * Returns the number of attributes in node.
+     * Implements Countable
+     *
+     * @return int
+     */
+    public function count()
+    {
+        return count($this->_currentData);
+    }
+}

+ 209 - 0
library/Zend/Ldap/Node/ChildrenIterator.php

@@ -0,0 +1,209 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Node
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Ldap_Node
+ */
+require_once 'Zend/Ldap/Node.php';
+
+/**
+ * Zend_Ldap_Node_ChildrenIterator provides an iterator to a collection of children nodes.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Node
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Node_ChildrenIterator implements Iterator, Countable, RecursiveIterator, ArrayAccess
+{
+    /**
+     * An array of Zend_Ldap_Node objects
+     *
+     * @var array
+     */
+    private $_data;
+
+    /**
+     * Constructor.
+     *
+     * @param array $data
+     * @return void
+     */
+    public function __construct(array $data)
+    {
+        $this->_data = $data;
+    }
+
+    /**
+     * Returns the number of child nodes.
+     * Implements Countable
+     *
+     * @return int
+     */
+    public function count()
+    {
+        return count($this->_data);
+    }
+
+    /**
+     * Return the current child.
+     * Implements Iterator
+     *
+     * @return Zend_Ldap_Node
+     */
+    public function current()
+    {
+        return current($this->_data);
+    }
+
+    /**
+     * Return the child'd RDN.
+     * Implements Iterator
+     *
+     * @return string
+     */
+    public function key()
+    {
+        return key($this->_data);
+    }
+
+    /**
+     * Move forward to next child.
+     * Implements Iterator
+     */
+    public function next()
+    {
+        next($this->_data);
+    }
+
+    /**
+     * Rewind the Iterator to the first child.
+     * Implements Iterator
+     */
+    public function rewind()
+    {
+        reset($this->_data);
+    }
+
+    /**
+     * Check if there is a current child
+     * after calls to rewind() or next().
+     * Implements Iterator
+     *
+     * @return boolean
+     */
+    public function valid()
+    {
+        return (current($this->_data)!==false);
+    }
+
+    /**
+     * Checks if current node has children.
+     * Returns whether the current element has children.
+     *
+     * @return boolean
+     */
+    public function hasChildren()
+    {
+        if ($this->current() instanceof Zend_Ldap_Node) {
+            return $this->current()->hasChildren();
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Returns the children for the current node.
+     *
+     * @return Zend_Ldap_Node_ChildrenIterator
+     */
+    public function getChildren()
+    {
+        if ($this->current() instanceof Zend_Ldap_Node) {
+            return $this->current()->getChildren();
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns a child with a given RDN.
+     * Implements ArrayAccess.
+     *
+     * @param  string $rdn
+     * @return Zend_Ldap_node
+     */
+    public function offsetGet($rdn)
+    {
+        if ($this->offsetExists($rdn)) {
+            return $this->_data[$rdn];
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Checks whether a given rdn exists.
+     * Implements ArrayAccess.
+     *
+     * @param  string $rdn
+     * @return boolean
+     */
+    public function offsetExists($rdn)
+    {
+        return (array_key_exists($rdn, $this->_data));
+    }
+
+    /**
+     * Does nothing.
+     * Implements ArrayAccess.
+     *
+     * @param  string $name
+     * @return null
+     */
+    public function offsetUnset($name) { }
+
+    /**
+     * Does nothing.
+     * Implements ArrayAccess.
+     *
+     * @param  string $name
+     * @param  mixed $value
+     * @return null
+     */
+    public function offsetSet($name, $value) { }
+
+    /**
+     * Get all children as an array
+     *
+     * @return array
+     */
+    public function toArray()
+    {
+        $data = array();
+        foreach ($this as $rdn => $node) {
+            $data[$rdn] = $node;
+        }
+        return $data;
+    }
+}

+ 67 - 0
library/Zend/Ldap/Node/Collection.php

@@ -0,0 +1,67 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Node
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Ldap_Collection
+ */
+require_once 'Zend/Ldap/Collection.php';
+
+
+/**
+ * Zend_Ldap_Node_Collection provides a collecion of nodes.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Node
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Node_Collection extends Zend_Ldap_Collection
+{
+    /**
+     * Creates the data structure for the given entry data
+     *
+     * @param array $data
+     * @return Zend_Ldap_Node
+     */
+    protected function _createEntry(array $data)
+    {
+        /**
+         * @see Zend_Ldap_Node
+         */
+        require_once 'Zend/Ldap/Node.php';
+        $node = Zend_Ldap_Node::fromArray($data, true);
+        $node->attachLdap($this->_iterator->getLdap());
+        return $node;
+    }
+
+    /**
+     * Return the child key (DN).
+     * Implements Iterator and RecursiveIterator
+     *
+     * @return string
+     */
+    public function key()
+    {
+        return $this->_iterator->key();
+    }
+}

+ 158 - 0
library/Zend/Ldap/Node/RootDse.php

@@ -0,0 +1,158 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage RootDSE
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Ldap_Node_Abstract
+ */
+require_once 'Zend/Ldap/Node/Abstract.php';
+
+/**
+ * Zend_Ldap_Node_RootDse provides a simple data-container for the RootDSE node.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage RootDSE
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Node_RootDse extends Zend_Ldap_Node_Abstract
+{
+    const SERVER_TYPE_GENERIC         = 1;
+    const SERVER_TYPE_OPENLDAP        = 2;
+    const SERVER_TYPE_ACTIVEDIRECTORY = 3;
+    const SERVER_TYPE_EDIRECTORY      = 4;
+
+    /**
+     * Factory method to create the RootDSE.
+     *
+     * @param  Zend_Ldap $ldap
+     * @return Zend_Ldap_Node_RootDse
+     * @throws Zend_Ldap_Exception
+     */
+    public static function create(Zend_Ldap $ldap)
+    {
+        $dn = Zend_Ldap_Dn::fromString('');
+        $data = $ldap->getEntry($dn, array('*', '+'), true);
+        if (isset($data['domainfunctionality'])) {
+            /**
+             * @see Zend_Ldap_Node_RootDse_ActiveDirectory
+             */
+            require_once 'Zend/Ldap/Node/RootDse/ActiveDirectory.php';
+            return new Zend_Ldap_Node_RootDse_ActiveDirectory($dn, $data);
+        } else if (isset($data['dsaname'])) {
+            /**
+             * @see Zend_Ldap_Node_RootDse_ActiveDirectory
+             */
+            require_once 'Zend/Ldap/Node/RootDse/eDirectory.php';
+            return new Zend_Ldap_Node_RootDse_eDirectory($dn, $data);
+        } else if (isset($data['structuralobjectclass']) &&
+                $data['structuralobjectclass'][0] === 'OpenLDAProotDSE') {
+            /**
+             * @see Zend_Ldap_Node_RootDse_OpenLdap
+             */
+            require_once 'Zend/Ldap/Node/RootDse/OpenLdap.php';
+            return new Zend_Ldap_Node_RootDse_OpenLdap($dn, $data);
+        } else {
+            return new self($dn, $data);
+        }
+    }
+
+    /**
+     * Constructor.
+     *
+     * Constructor is protected to enforce the use of factory methods.
+     *
+     * @param  Zend_Ldap_Dn $dn
+     * @param  array $data
+     */
+    protected function __construct(Zend_Ldap_Dn $dn, array $data)
+    {
+        parent::__construct($dn, $data, true);
+    }
+
+    /**
+     * Gets the namingContexts.
+     *
+     * @return array
+     */
+    public function getNamingContexts()
+    {
+        return $this->getAttribute('namingContexts', null);
+    }
+
+    /**
+     * Gets the subschemaSubentry.
+     *
+     * @return string|null
+     */
+    public function getSubschemaSubentry()
+    {
+        return $this->getAttribute('subschemaSubentry', 0);
+    }
+
+    /**
+     * Determines if the version is supported
+     *
+     * @param string|int|array $versions version(s) to check
+     * @return boolean
+     */
+    public function supportsVersion($versions)
+    {
+        return $this->attributeHasValue('supportedLDAPVersion', $versions);
+    }
+
+    /**
+     * Determines if the sasl mechanism is supported
+     *
+     * @param string|array $mechlist SASL mechanisms to check
+     * @return boolean
+     */
+    public function supportsSaslMechanism($mechlist)
+    {
+        return $this->attributeHasValue('supportedSASLMechanisms', $mechlist);
+    }
+
+    /**
+     * Gets the server type
+     *
+     * @return int
+     */
+    public function getServerType()
+    {
+        return self::SERVER_TYPE_GENERIC;
+    }
+
+    /**
+     * Returns the schema DN
+     *
+     * @return Zend_Ldap_Dn
+     */
+    public function getSchemaDn()
+    {
+        $schemaDn = $this->getSubschemaSubentry();
+        /**
+         * @see Zend_Ldap_Dn
+         */
+        require_once 'Zend/Ldap/Dn.php';
+        return Zend_Ldap_Dn::fromString($schemaDn);
+    }
+}

+ 247 - 0
library/Zend/Ldap/Node/RootDse/ActiveDirectory.php

@@ -0,0 +1,247 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage RootDSE
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Ldap_Node_RootDse
+ */
+require_once 'Zend/Ldap/Node/RootDse.php';
+
+/**
+ * Zend_Ldap_Node_RootDse provides a simple data-container for the RootDSE node of
+ * an Active Directory server.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage RootDSE
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Node_RootDse_ActiveDirectory extends Zend_Ldap_Node_RootDse
+{
+    /**
+     * Gets the configurationNamingContext.
+     *
+     * @return string|null
+     */
+    public function getConfigurationNamingContext()
+    {
+        return $this->getAttribute('configurationNamingContext', 0);
+    }
+
+    /**
+     * Gets the currentTime.
+     *
+     * @return string|null
+     */
+    public function getCurrentTime()
+    {
+        return $this->getAttribute('currentTime', 0);
+    }
+
+    /**
+     * Gets the defaultNamingContext.
+     *
+     * @return string|null
+     */
+    public function getDefaultNamingContext()
+    {
+        return $this->getAttribute('defaultNamingContext', 0);
+    }
+
+    /**
+     * Gets the dnsHostName.
+     *
+     * @return string|null
+     */
+    public function getDnsHostName()
+    {
+        return $this->getAttribute('dnsHostName', 0);
+    }
+
+    /**
+     * Gets the domainControllerFunctionality.
+     *
+     * @return string|null
+     */
+    public function getDomainControllerFunctionality()
+    {
+        return $this->getAttribute('domainControllerFunctionality', 0);
+    }
+
+    /**
+     * Gets the domainFunctionality.
+     *
+     * @return string|null
+     */
+    public function getDomainFunctionality()
+    {
+        return $this->getAttribute('domainFunctionality', 0);
+    }
+
+    /**
+     * Gets the dsServiceName.
+     *
+     * @return string|null
+     */
+    public function getDsServiceName()
+    {
+        return $this->getAttribute('dsServiceName', 0);
+    }
+
+    /**
+     * Gets the forestFunctionality.
+     *
+     * @return string|null
+     */
+    public function getForestFunctionality()
+    {
+        return $this->getAttribute('forestFunctionality', 0);
+    }
+
+    /**
+     * Gets the highestCommittedUSN.
+     *
+     * @return string|null
+     */
+    public function getHighestCommittedUSN()
+    {
+        return $this->getAttribute('highestCommittedUSN', 0);
+    }
+
+    /**
+     * Gets the isGlobalCatalogReady.
+     *
+     * @return string|null
+     */
+    public function getIsGlobalCatalogReady()
+    {
+        return $this->getAttribute('isGlobalCatalogReady', 0);
+    }
+
+    /**
+     * Gets the isSynchronized.
+     *
+     * @return string|null
+     */
+    public function getIsSynchronized()
+    {
+        return $this->getAttribute('isSynchronized', 0);
+    }
+
+    /**
+     * Gets the ldapServiceName.
+     *
+     * @return string|null
+     */
+    public function getLdapServiceName()
+    {
+        return $this->getAttribute('ldapServiceName', 0);
+    }
+
+    /**
+     * Gets the rootDomainNamingContext.
+     *
+     * @return string|null
+     */
+    public function getRootDomainNamingContext()
+    {
+        return $this->getAttribute('rootDomainNamingContext', 0);
+    }
+
+    /**
+     * Gets the schemaNamingContext.
+     *
+     * @return string|null
+     */
+    public function getSchemaNamingContext()
+    {
+        return $this->getAttribute('schemaNamingContext', 0);
+    }
+
+    /**
+     * Gets the serverName.
+     *
+     * @return string|null
+     */
+    public function getServerName()
+    {
+        return $this->getAttribute('serverName', 0);
+    }
+
+    /**
+     * Determines if the capability is supported
+     *
+     * @param string|string|array $oids capability(s) to check
+     * @return boolean
+     */
+    public function supportsCapability($oids)
+    {
+        return $this->attributeHasValue('supportedCapabilities', $oids);
+    }
+
+    /**
+     * Determines if the control is supported
+     *
+     * @param string|array $oids control oid(s) to check
+     * @return boolean
+     */
+    public function supportsControl($oids)
+    {
+        return $this->attributeHasValue('supportedControl', $oids);
+    }
+
+    /**
+     * Determines if the version is supported
+     *
+     * @param string|array $policies policy(s) to check
+     * @return boolean
+     */
+    public function supportsPolicy($policies)
+    {
+        return $this->attributeHasValue('supportedLDAPPolicies', $policies);
+    }
+
+    /**
+     * Gets the server type
+     *
+     * @return int
+     */
+    public function getServerType()
+    {
+        return self::SERVER_TYPE_ACTIVEDIRECTORY;
+    }
+
+    /**
+     * Returns the schema DN
+     *
+     * @return Zend_Ldap_Dn
+     */
+    public function getSchemaDn()
+    {
+        $schemaDn = $this->getSchemaNamingContext();
+        /**
+         * @see Zend_Ldap_Dn
+         */
+        require_once 'Zend/Ldap/Dn.php';
+        return Zend_Ldap_Dn::fromString($schemaDn);
+    }
+}

+ 102 - 0
library/Zend/Ldap/Node/RootDse/OpenLdap.php

@@ -0,0 +1,102 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage RootDSE
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Ldap_Node_RootDse
+ */
+require_once 'Zend/Ldap/Node/RootDse.php';
+
+/**
+ * Zend_Ldap_Node_RootDse provides a simple data-container for the RootDSE node of
+ * an OpenLDAP server.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage RootDSE
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Node_RootDse_OpenLdap extends Zend_Ldap_Node_RootDse
+{
+    /**
+     * Gets the configContext.
+     *
+     * @return string|null
+     */
+    public function getConfigContext()
+    {
+        return $this->getAttribute('configContext', 0);
+    }
+
+    /**
+     * Gets the monitorContext.
+     *
+     * @return string|null
+     */
+    public function getMonitorContext()
+    {
+        return $this->getAttribute('monitorContext', 0);
+    }
+
+    /**
+     * Determines if the control is supported
+     *
+     * @param string|array $oids control oid(s) to check
+     * @return boolean
+     */
+    public function supportsControl($oids)
+    {
+        return $this->attributeHasValue('supportedControl', $oids);
+    }
+
+    /**
+     * Determines if the extension is supported
+     *
+     * @param string|array $oids oid(s) to check
+     * @return boolean
+     */
+    public function supportsExtension($oids)
+    {
+        return $this->attributeHasValue('supportedExtension', $oids);
+    }
+
+    /**
+     * Determines if the feature is supported
+     *
+     * @param string|array $oids feature oid(s) to check
+     * @return boolean
+     */
+    public function supportsFeature($oids)
+    {
+        return $this->attributeHasValue('supportedFeatures', $oids);
+    }
+
+    /**
+     * Gets the server type
+     *
+     * @return int
+     */
+    public function getServerType()
+    {
+        return self::SERVER_TYPE_OPENLDAP;
+    }
+}

+ 160 - 0
library/Zend/Ldap/Node/RootDse/eDirectory.php

@@ -0,0 +1,160 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage RootDSE
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Ldap_Node_RootDse
+ */
+require_once 'Zend/Ldap/Node/RootDse.php';
+
+/**
+ * Zend_Ldap_Node_RootDse provides a simple data-container for the RootDSE node of
+ * a Novell eDirectory server.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage RootDSE
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Node_RootDse_eDirectory extends Zend_Ldap_Node_RootDse
+{
+    /**
+     * Determines if the extension is supported
+     *
+     * @param string|array $oids oid(s) to check
+     * @return boolean
+     */
+    public function supportsExtension($oids)
+    {
+        return $this->attributeHasValue('supportedExtension', $oids);
+    }
+
+    /**
+     * Gets the vendorName.
+     *
+     * @return string|null
+     */
+    public function getVendorName()
+    {
+        return $this->getAttribute('vendorName', 0);
+    }
+
+    /**
+     * Gets the vendorVersion.
+     *
+     * @return string|null
+     */
+    public function getVendorVersion()
+    {
+        return $this->getAttribute('vendorVersion', 0);
+    }
+
+    /**
+     * Gets the dsaName.
+     *
+     * @return string|null
+     */
+    public function getDsaName()
+    {
+        return $this->getAttribute('dsaName', 0);
+    }
+
+    /**
+     * Gets the server statistics "errors".
+     *
+     * @return string|null
+     */
+    public function getStatisticsErrors()
+    {
+        return $this->getAttribute('errors', 0);
+    }
+
+    /**
+     * Gets the server statistics "securityErrors".
+     *
+     * @return string|null
+     */
+    public function getStatisticsSecurityErrors()
+    {
+        return $this->getAttribute('securityErrors', 0);
+    }
+
+    /**
+     * Gets the server statistics "chainings".
+     *
+     * @return string|null
+     */
+    public function getStatisticsChainings()
+    {
+        return $this->getAttribute('chainings', 0);
+    }
+
+    /**
+     * Gets the server statistics "referralsReturned".
+     *
+     * @return string|null
+     */
+    public function getStatisticsReferralsReturned()
+    {
+        return $this->getAttribute('referralsReturned', 0);
+    }
+
+    /**
+     * Gets the server statistics "extendedOps".
+     *
+     * @return string|null
+     */
+    public function getStatisticsExtendedOps()
+    {
+        return $this->getAttribute('extendedOps', 0);
+    }
+
+    /**
+     * Gets the server statistics "abandonOps".
+     *
+     * @return string|null
+     */
+    public function getStatisticsAbandonOps()
+    {
+        return $this->getAttribute('abandonOps', 0);
+    }
+
+    /**
+     * Gets the server statistics "wholeSubtreeSearchOps".
+     *
+     * @return string|null
+     */
+    public function getStatisticsWholeSubtreeSearchOps()
+    {
+        return $this->getAttribute('wholeSubtreeSearchOps', 0);
+    }
+
+    /**
+     * Gets the server type
+     *
+     * @return int
+     */
+    public function getServerType()
+    {
+        return self::SERVER_TYPE_EDIRECTORY;
+    }
+}

+ 120 - 0
library/Zend/Ldap/Node/Schema.php

@@ -0,0 +1,120 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Schema
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Ldap_Node_Abstract
+ */
+require_once 'Zend/Ldap/Node/Abstract.php';
+
+/**
+ * Zend_Ldap_Node_Schema provides a simple data-container for the Schema node.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Schema
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Node_Schema extends Zend_Ldap_Node_Abstract
+{
+    const OBJECTCLASS_TYPE_UNKNOWN    = 0;
+    const OBJECTCLASS_TYPE_STRUCTURAL = 1;
+    const OBJECTCLASS_TYPE_ABSTRACT   = 3;
+    const OBJECTCLASS_TYPE_AUXILIARY  = 4;
+
+    /**
+     * Factory method to create the Schema node.
+     *
+     * @param  Zend_Ldap $ldap
+     * @return Zend_Ldap_Node_Schema
+     * @throws Zend_Ldap_Exception
+     */
+    public static function create(Zend_Ldap $ldap)
+    {
+        $dn = $ldap->getRootDse()->getSchemaDn();
+        $data = $ldap->getEntry($dn, array('*', '+'), true);
+        switch ($ldap->getRootDse()->getServerType()) {
+            case Zend_Ldap_Node_RootDse::SERVER_TYPE_ACTIVEDIRECTORY:
+                /**
+                 * @see Zend_Ldap_Node_Schema_ActiveDirectory
+                 */
+                require_once 'Zend/Ldap/Node/Schema/ActiveDirectory.php';
+                return new Zend_Ldap_Node_Schema_ActiveDirectory($dn, $data, $ldap);
+            case Zend_Ldap_Node_RootDse::SERVER_TYPE_OPENLDAP:
+                /**
+                 * @see Zend_Ldap_Node_RootDse_ActiveDirectory
+                 */
+                require_once 'Zend/Ldap/Node/Schema/OpenLdap.php';
+                return new Zend_Ldap_Node_Schema_OpenLdap($dn, $data, $ldap);
+            case Zend_Ldap_Node_RootDse::SERVER_TYPE_EDIRECTORY:
+            default:
+                return new self($dn, $data, $ldap);
+        }
+    }
+
+    /**
+     * Constructor.
+     *
+     * Constructor is protected to enforce the use of factory methods.
+     *
+     * @param  Zend_Ldap_Dn $dn
+     * @param  array $data
+     * @param  Zend_Ldap $ldap
+     */
+    protected function __construct(Zend_Ldap_Dn $dn, array $data, Zend_Ldap $ldap)
+    {
+        parent::__construct($dn, $data, true);
+        $this->_parseSchema($dn, $ldap);
+    }
+
+    /**
+     * Parses the schema
+     *
+     * @param  Zend_Ldap_Dn $dn
+     * @param  Zend_Ldap $ldap
+     * @return Zend_Ldap_Node_Schema *Provides a fluid interface*
+     */
+    protected function _parseSchema(Zend_Ldap_Dn $dn, Zend_Ldap $ldap)
+    {
+        return $this;
+    }
+
+    /**
+     * Gets the attribute Types
+     *
+     * @return array
+     */
+    public function getAttributeTypes()
+    {
+        return array();
+    }
+
+    /**
+     * Gets the object classes
+     *
+     * @return array
+     */
+    public function getObjectClasses()
+    {
+        return array();
+    }
+}

+ 103 - 0
library/Zend/Ldap/Node/Schema/ActiveDirectory.php

@@ -0,0 +1,103 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Schema
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Ldap_Node_Schema
+ */
+require_once 'Zend/Ldap/Node/Schema.php';
+/**
+ * @see Zend_Ldap_Node_Schema_AttributeType_ActiveDirectory
+ */
+require_once 'Zend/Ldap/Node/Schema/AttributeType/ActiveDirectory.php';
+/**
+ * @see Zend_Ldap_Node_Schema_ObjectClass_ActiveDirectory
+ */
+require_once 'Zend/Ldap/Node/Schema/ObjectClass/ActiveDirectory.php';
+
+/**
+ * Zend_Ldap_Node_Schema_ActiveDirectory provides a simple data-container for the Schema node of
+ * an Active Directory server.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Schema
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Node_Schema_ActiveDirectory extends Zend_Ldap_Node_Schema
+{
+    /**
+     * The attribute Types
+     *
+     * @var array
+     */
+    protected $_attributeTypes = array();
+    /**
+     * The object classes
+     *
+     * @var array
+     */
+    protected $_objectClasses = array();
+
+    /**
+     * Parses the schema
+     *
+     * @param  Zend_Ldap_Dn $dn
+     * @param  Zend_Ldap $ldap
+     * @return Zend_Ldap_Node_Schema *Provides a fluid interface*
+     */
+    protected function _parseSchema(Zend_Ldap_Dn $dn, Zend_Ldap $ldap)
+    {
+        parent::_parseSchema($dn, $ldap);
+        foreach ($ldap->search('(objectClass=classSchema)', $dn,
+                Zend_Ldap::SEARCH_SCOPE_ONE) as $node) {
+            $val = new Zend_Ldap_Node_Schema_ObjectClass_ActiveDirectory($node);
+            $this->_objectClasses[$val->getName()] = $val;
+        }
+        foreach ($ldap->search('(objectClass=attributeSchema)', $dn,
+                Zend_Ldap::SEARCH_SCOPE_ONE) as $node) {
+            $val = new Zend_Ldap_Node_Schema_AttributeType_ActiveDirectory($node);
+            $this->_attributeTypes[$val->getName()] = $val;
+        }
+        return $this;
+    }
+
+    /**
+     * Gets the attribute Types
+     *
+     * @return array
+     */
+    public function getAttributeTypes()
+    {
+        return $this->_attributeTypes;
+    }
+
+    /**
+     * Gets the object classes
+     *
+     * @return array
+     */
+    public function getObjectClasses()
+    {
+        return $this->_objectClasses;
+    }
+}

+ 104 - 0
library/Zend/Ldap/Node/Schema/AttributeType/ActiveDirectory.php

@@ -0,0 +1,104 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Schema
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Ldap_Node_Schema_Item
+ */
+require_once 'Zend/Ldap/Node/Schema/Item.php';
+/**
+ * @see Zend_Ldap_Node_Schema_AttributeType_Interface
+ */
+require_once 'Zend/Ldap/Node/Schema/AttributeType/Interface.php';
+
+/**
+ * Zend_Ldap_Node_Schema_AttributeType_ActiveDirectory provides access to the attribute type
+ * schema information on an Active Directory server.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Schema
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Node_Schema_AttributeType_ActiveDirectory extends Zend_Ldap_Node_Schema_Item
+    implements Zend_Ldap_Node_Schema_AttributeType_Interface
+{
+    /**
+     * Gets the attribute name
+     *
+     * @return string
+     */
+    public function getName()
+    {
+        return $this->ldapdisplayname[0];
+    }
+
+    /**
+     * Gets the attribute OID
+     *
+     * @return string
+     */
+    public function getOid()
+    {
+
+    }
+
+    /**
+     * Gets the attribute syntax
+     *
+     * @return string
+     */
+    public function getSyntax()
+    {
+
+    }
+
+    /**
+     * Gets the attribute maximum length
+     *
+     * @return int|null
+     */
+    public function getMaxLength()
+    {
+
+    }
+
+    /**
+     * Returns if the attribute is single-valued.
+     *
+     * @return boolean
+     */
+    public function isSingleValued()
+    {
+
+    }
+
+    /**
+     * Gets the attribute description
+     *
+     * @return string
+     */
+    public function getDescription()
+    {
+
+    }
+}

+ 75 - 0
library/Zend/Ldap/Node/Schema/AttributeType/Interface.php

@@ -0,0 +1,75 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Schema
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * Zend_Ldap_Node_Schema_AttributeType_Interface provides a contract for schema attribute-types.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Schema
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+interface Zend_Ldap_Node_Schema_AttributeType_Interface
+{
+    /**
+     * Gets the attribute name
+     *
+     * @return string
+     */
+    public function getName();
+
+    /**
+     * Gets the attribute OID
+     *
+     * @return string
+     */
+    public function getOid();
+
+    /**
+     * Gets the attribute syntax
+     *
+     * @return string
+     */
+    public function getSyntax();
+
+    /**
+     * Gets the attribute maximum length
+     *
+     * @return int|null
+     */
+    public function getMaxLength();
+
+    /**
+     * Returns if the attribute is single-valued.
+     *
+     * @return boolean
+     */
+    public function isSingleValued();
+
+    /**
+     * Gets the attribute description
+     *
+     * @return string
+     */
+    public function getDescription();
+}

+ 129 - 0
library/Zend/Ldap/Node/Schema/AttributeType/OpenLdap.php

@@ -0,0 +1,129 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Schema
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Ldap_Node_Schema_Item
+ */
+require_once 'Zend/Ldap/Node/Schema/Item.php';
+/**
+ * @see Zend_Ldap_Node_Schema_AttributeType_Interface
+ */
+require_once 'Zend/Ldap/Node/Schema/AttributeType/Interface.php';
+
+/**
+ * Zend_Ldap_Node_Schema_AttributeType_OpenLdap provides access to the attribute type
+ * schema information on an OpenLDAP server.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Schema
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Node_Schema_AttributeType_OpenLdap extends Zend_Ldap_Node_Schema_Item
+    implements Zend_Ldap_Node_Schema_AttributeType_Interface
+{
+    /**
+     * Gets the attribute name
+     *
+     * @return string
+     */
+    public function getName()
+    {
+        return $this->name;
+    }
+
+    /**
+     * Gets the attribute OID
+     *
+     * @return string
+     */
+    public function getOid()
+    {
+        return $this->oid;
+    }
+
+    /**
+     * Gets the attribute syntax
+     *
+     * @return string
+     */
+    public function getSyntax()
+    {
+        if ($this->syntax === null) {
+            $parent = $this->getParent();
+            if ($parent === null) return null;
+            else return $parent->getSyntax();
+        } else {
+            return $this->syntax;
+        }
+    }
+
+    /**
+     * Gets the attribute maximum length
+     *
+     * @return int|null
+     */
+    public function getMaxLength()
+    {
+        $maxLength = $this->{'max-length'};
+        if ($maxLength === null) {
+            $parent = $this->getParent();
+            if ($parent === null) return null;
+            else return $parent->getMaxLength();
+        } else {
+            return (int)$maxLength;
+        }
+    }
+
+    /**
+     * Returns if the attribute is single-valued.
+     *
+     * @return boolean
+     */
+    public function isSingleValued()
+    {
+        return $this->{'single-value'};
+    }
+
+    /**
+     * Gets the attribute description
+     *
+     * @return string
+     */
+    public function getDescription()
+    {
+        return $this->desc;
+    }
+
+    /**
+     * Returns the parent attribute type in the inhertitance tree if one exists
+     *
+     * @return Zend_Ldap_Node_Schema_AttributeType_OpenLdap|null
+     */
+    public function getParent()
+    {
+        if (count($this->_parents) === 1) {
+            return $this->_parents[0];
+        }
+    }
+}

+ 163 - 0
library/Zend/Ldap/Node/Schema/Item.php

@@ -0,0 +1,163 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Schema
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * Zend_Ldap_Node_Schema_Item provides a base implementation for managing schema
+ * items like objectClass and attribute.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Schema
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+abstract class Zend_Ldap_Node_Schema_Item implements ArrayAccess, Countable
+{
+    /**
+     * The underlying data
+     *
+     * @var array
+     */
+    protected $_data;
+
+    /**
+     * Constructor.
+     *
+     * @param array $data
+     */
+    public function __construct(array $data)
+    {
+        $this->setData($data);
+    }
+
+    /**
+     * Sets the data
+     *
+     * @param array $data
+     * @return Zend_Ldap_Node_Schema_Item *Provides a fluid interface*
+     */
+    public function setData(array $data)
+    {
+        $this->_data = $data;
+        return $this;
+    }
+
+    /**
+     * Gets the data
+     *
+     * @return array
+     */
+    public function getData()
+    {
+        return $this->_data;
+    }
+
+    /**
+     * Gets a specific attribute from this item
+     *
+     * @param  string $name
+     * @return mixed
+     */
+    public function __get($name)
+    {
+        if (array_key_exists($name, $this->_data)) {
+            return $this->_data[$name];
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Checks whether a specific attribute exists.
+     *
+     * @param  string $name
+     * @return boolean
+     */
+    public function __isset($name)
+    {
+        return (array_key_exists($name, $this->_data));
+    }
+
+    /**
+     * Always throws BadMethodCallException
+     * Implements ArrayAccess.
+     *
+     * This method is needed for a full implementation of ArrayAccess
+     *
+     * @param  string $name
+     * @param  mixed $value
+     * @return null
+     * @throws BadMethodCallException
+     */
+    public function offsetSet($name, $value)
+    {
+        throw new BadMethodCallException();
+    }
+
+    /**
+     * Gets a specific attribute from this item
+     *
+     * @param  string $name
+     * @return mixed
+     */
+    public function offsetGet($name)
+    {
+        return $this->__get($name);
+    }
+
+    /**
+     * Always throws BadMethodCallException
+     * Implements ArrayAccess.
+     *
+     * This method is needed for a full implementation of ArrayAccess
+     *
+     * @param  string $name
+     * @return null
+     * @throws BadMethodCallException
+     */
+    public function offsetUnset($name)
+    {
+        throw new BadMethodCallException();
+    }
+
+    /**
+     * Checks whether a specific attribute exists.
+     *
+     * @param  string $name
+     * @return boolean
+     */
+    public function offsetExists($name)
+    {
+        return $this->__isset($name);
+    }
+
+    /**
+     * Returns the number of attributes.
+     * Implements Countable
+     *
+     * @return int
+     */
+    public function count()
+    {
+        return count($this->_data);
+    }
+}

+ 115 - 0
library/Zend/Ldap/Node/Schema/ObjectClass/ActiveDirectory.php

@@ -0,0 +1,115 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Schema
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Ldap_Node_Schema_Item
+ */
+require_once 'Zend/Ldap/Node/Schema/Item.php';
+/**
+ * @see Zend_Ldap_Node_Schema_ObjectClass_Interface
+ */
+require_once 'Zend/Ldap/Node/Schema/ObjectClass/Interface.php';
+
+/**
+ * Zend_Ldap_Node_Schema_ObjectClass_ActiveDirectory provides access to the objectClass
+ * schema information on an Active Directory server.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Schema
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Node_Schema_ObjectClass_ActiveDirectory extends Zend_Ldap_Node_Schema_Item
+    implements Zend_Ldap_Node_Schema_ObjectClass_Interface
+{
+    /**
+     * Gets the objectClass name
+     *
+     * @return string
+     */
+    public function getName()
+    {
+        return $this->ldapdisplayname[0];
+    }
+
+    /**
+     * Gets the objectClass OID
+     *
+     * @return string
+     */
+    public function getOid()
+    {
+
+    }
+
+    /**
+     * Gets the attributes that this objectClass must contain
+     *
+     * @return array
+     */
+    public function getMustContain()
+    {
+
+    }
+
+    /**
+     * Gets the attributes that this objectClass may contain
+     *
+     * @return array
+     */
+    public function getMayContain()
+    {
+
+    }
+
+    /**
+     * Gets the objectClass description
+     *
+     * @return string
+     */
+    public function getDescription()
+    {
+
+    }
+
+    /**
+     * Gets the objectClass type
+     *
+     * @return integer
+     */
+    public function getType()
+    {
+
+    }
+
+    /**
+     * Returns the parent objectClasses of this class.
+     * This includes structural, abstract and auxiliary objectClasses
+     *
+     * @return array
+     */
+    public function getParentClasses()
+    {
+
+    }
+}

+ 83 - 0
library/Zend/Ldap/Node/Schema/ObjectClass/Interface.php

@@ -0,0 +1,83 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Schema
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * Zend_Ldap_Node_Schema_ObjectClass_Interface provides a contract for schema objectClasses.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Schema
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+interface Zend_Ldap_Node_Schema_ObjectClass_Interface
+{
+    /**
+     * Gets the objectClass name
+     *
+     * @return string
+     */
+    public function getName();
+
+    /**
+     * Gets the objectClass OID
+     *
+     * @return string
+     */
+    public function getOid();
+
+    /**
+     * Gets the attributes that this objectClass must contain
+     *
+     * @return array
+     */
+    public function getMustContain();
+
+    /**
+     * Gets the attributes that this objectClass may contain
+     *
+     * @return array
+     */
+    public function getMayContain();
+
+    /**
+     * Gets the objectClass description
+     *
+     * @return string
+     */
+    public function getDescription();
+
+    /**
+     * Gets the objectClass type
+     *
+     * @return integer
+     */
+    public function getType();
+
+    /**
+     * Returns the parent objectClasses of this class.
+     * This includes structural, abstract and auxiliary objectClasses
+     *
+     * @return array
+     */
+    public function getParentClasses();
+}

+ 175 - 0
library/Zend/Ldap/Node/Schema/ObjectClass/OpenLdap.php

@@ -0,0 +1,175 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Schema
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Ldap_Node_Schema_Item
+ */
+require_once 'Zend/Ldap/Node/Schema/Item.php';
+/**
+ * @see Zend_Ldap_Node_Schema_ObjectClass_Interface
+ */
+require_once 'Zend/Ldap/Node/Schema/ObjectClass/Interface.php';
+
+/**
+ * Zend_Ldap_Node_Schema_ObjectClass_OpenLdap provides access to the objectClass
+ * schema information on an OpenLDAP server.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Schema
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Node_Schema_ObjectClass_OpenLdap extends Zend_Ldap_Node_Schema_Item
+    implements Zend_Ldap_Node_Schema_ObjectClass_Interface
+{
+    /**
+     * All inherited "MUST" attributes
+     *
+     * @var array
+     */
+    protected $_inheritedMust = null;
+    /**
+     * All inherited "MAY" attributes
+     *
+     * @var array
+     */
+    protected $_inheritedMay = null;
+
+
+    /**
+     * Gets the objectClass name
+     *
+     * @return string
+     */
+    public function getName()
+    {
+        return $this->name;
+    }
+
+    /**
+     * Gets the objectClass OID
+     *
+     * @return string
+     */
+    public function getOid()
+    {
+        return $this->oid;
+    }
+
+    /**
+     * Gets the attributes that this objectClass must contain
+     *
+     * @return array
+     */
+    public function getMustContain()
+    {
+        if ($this->_inheritedMust === null) {
+            $this->_resolveInheritance();
+        }
+        return $this->_inheritedMust;
+    }
+
+    /**
+     * Gets the attributes that this objectClass may contain
+     *
+     * @return array
+     */
+    public function getMayContain()
+    {
+        if ($this->_inheritedMay === null) {
+            $this->_resolveInheritance();
+        }
+        return $this->_inheritedMay;
+    }
+
+    /**
+     * Resolves the inheritance tree
+     *
+     * @return void
+     */
+    protected function _resolveInheritance()
+    {
+        $must = $this->must;
+        $may = $this->may;
+        foreach ($this->getParents() as $p) {
+            $must = array_merge($must, $p->getMustContain());
+            $may = array_merge($may, $p->getMayContain());
+        }
+        $must = array_unique($must);
+        $may = array_unique($may);
+        $may = array_diff($may, $must);
+        sort($must, SORT_STRING);
+        sort($may, SORT_STRING);
+        $this->_inheritedMust = $must;
+        $this->_inheritedMay = $may;
+    }
+
+    /**
+     * Gets the objectClass description
+     *
+     * @return string
+     */
+    public function getDescription()
+    {
+        return $this->desc;
+    }
+
+    /**
+     * Gets the objectClass type
+     *
+     * @return integer
+     */
+    public function getType()
+    {
+        if ($this->structural) {
+            return Zend_Ldap_Node_Schema::OBJECTCLASS_TYPE_STRUCTURAL;
+        } else if ($this->abstract) {
+            return Zend_Ldap_Node_Schema::OBJECTCLASS_TYPE_ABSTRACT;
+        } else if ($this->auxiliary) {
+            return Zend_Ldap_Node_Schema::OBJECTCLASS_TYPE_AUXILIARY;
+        } else {
+            return Zend_Ldap_Node_Schema::OBJECTCLASS_TYPE_UNKNOWN;
+        }
+    }
+
+    /**
+     * Returns the parent objectClasses of this class.
+     * This includes structural, abstract and auxiliary objectClasses
+     *
+     * @return array
+     */
+    public function getParentClasses()
+    {
+        return $this->sup;
+    }
+
+    /**
+     * Returns the parent object classes in the inhertitance tree if one exists
+     *
+     * @return array of Zend_Ldap_Node_Schema_ObjectClass_OpenLdap
+     */
+    public function getParents()
+    {
+        return $this->_parents;
+    }
+}

+ 502 - 0
library/Zend/Ldap/Node/Schema/OpenLdap.php

@@ -0,0 +1,502 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Schema
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * @see Zend_Ldap_Node_Schema
+ */
+require_once 'Zend/Ldap/Node/Schema.php';
+/**
+ * @see Zend_Ldap_Node_Schema_AttributeType_OpenLdap
+ */
+require_once 'Zend/Ldap/Node/Schema/AttributeType/OpenLdap.php';
+/**
+ * @see Zend_Ldap_Node_Schema_ObjectClass_OpenLdap
+ */
+require_once 'Zend/Ldap/Node/Schema/ObjectClass/OpenLdap.php';
+
+/**
+ * Zend_Ldap_Node_Schema_OpenLdap provides a simple data-container for the Schema node of
+ * an OpenLDAP server.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage Schema
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Node_Schema_OpenLdap extends Zend_Ldap_Node_Schema
+{
+    /**
+     * The attribute Types
+     *
+     * @var array
+     */
+    protected $_attributeTypes = null;
+    /**
+     * The object classes
+     *
+     * @var array
+     */
+    protected $_objectClasses = null;
+    /**
+     * The LDAP syntaxes
+     *
+     * @var array
+     */
+    protected $_ldapSyntaxes = null;
+    /**
+     * The matching rules
+     *
+     * @var array
+     */
+    protected $_matchingRules = null;
+    /**
+     * The matching rule use
+     *
+     * @var array
+     */
+    protected $_matchingRuleUse = null;
+
+    /**
+     * Parses the schema
+     *
+     * @param  Zend_Ldap_Dn $dn
+     * @param  Zend_Ldap $ldap
+     * @return Zend_Ldap_Node_Schema *Provides a fluid interface*
+     */
+    protected function _parseSchema(Zend_Ldap_Dn $dn, Zend_Ldap $ldap)
+    {
+        parent::_parseSchema($dn, $ldap);
+        $this->_loadAttributeTypes();
+        $this->_loadLdapSyntaxes();
+        $this->_loadMatchingRules();
+        $this->_loadMatchingRuleUse();
+        $this->_loadObjectClasses();
+        return $this;
+    }
+
+    /**
+     * Gets the attribute Types
+     *
+     * @return array
+     */
+    public function getAttributeTypes()
+    {
+        return $this->_attributeTypes;
+    }
+
+    /**
+     * Gets the object classes
+     *
+     * @return array
+     */
+    public function getObjectClasses()
+    {
+        return $this->_objectClasses;
+    }
+
+    /**
+     * Gets the LDAP syntaxes
+     *
+     * @return array
+     */
+    public function getLdapSyntaxes()
+    {
+        return $this->_ldapSyntaxes;
+    }
+
+    /**
+     * Gets the matching rules
+     *
+     * @return array
+     */
+    public function getMatchingRules()
+    {
+        return $this->_matchingRules;
+    }
+
+    /**
+     * Gets the matching rule use
+     *
+     * @return array
+     */
+    public function getMatchingRuleUse()
+    {
+        return $this->_matchingRuleUse;
+    }
+
+    /**
+     * Loads the attribute Types
+     *
+     * @return void
+     */
+    protected function _loadAttributeTypes()
+    {
+        $this->_attributeTypes = array();
+        foreach ($this->getAttribute('attributeTypes') as $value) {
+            $val = $this->_parseAttributeType($value);
+            $val = new Zend_Ldap_Node_Schema_AttributeType_OpenLdap($val);
+            $this->_attributeTypes[$val->getName()] = $val;
+
+        }
+        foreach ($this->_attributeTypes as $val) {
+            if (count($val->sup) > 0) {
+                $this->_resolveInheritance($val, $this->_attributeTypes);
+            }
+            foreach ($val->aliases as $alias) {
+                $this->_attributeTypes[$alias] = $val;
+            }
+        }
+        ksort($this->_attributeTypes, SORT_STRING);
+    }
+
+    /**
+     * Parses an attributeType value
+     *
+     * @param string $value
+     * @return array
+     */
+    protected function _parseAttributeType($value)
+    {
+        $attributeType = array(
+            'oid'                  => null,
+            'name'                 => null,
+            'desc'                 => null,
+            'obsolete'             => false,
+            'sup'                  => null,
+            'equality'             => null,
+            'ordering'             => null,
+            'substr'               => null,
+            'syntax'               => null,
+            'max-length'           => null,
+            'single-value'         => false,
+            'collective'           => false,
+            'no-user-modification' => false,
+            'usage'                => 'userApplications',
+            '_string'              => $value,
+            '_parents'             => array());
+
+        $tokens = $this->_tokenizeString($value);
+        $attributeType['oid'] = array_shift($tokens); // first token is the oid
+        $this->_parseLdapSchemaSyntax($attributeType, $tokens);
+
+        if (array_key_exists('syntax', $attributeType)) {
+            // get max length from syntax
+            if (preg_match('/^(.+){(\d+)}$/', $attributeType['syntax'], $matches)) {
+                $attributeType['syntax'] = $matches[1];
+                $attributeType['max-length'] = $matches[2];
+            }
+        }
+
+        $this->_ensureNameAttribute($attributeType);
+
+        return $attributeType;
+    }
+
+    /**
+     * Loads the object classes
+     *
+     * @return void
+     */
+    protected function _loadObjectClasses()
+    {
+        $this->_objectClasses = array();
+        foreach ($this->getAttribute('objectClasses') as $value) {
+            $val = $this->_parseObjectClass($value);
+            $val = new Zend_Ldap_Node_Schema_ObjectClass_OpenLdap($val);
+            $this->_objectClasses[$val->getName()] = $val;
+        }
+        foreach ($this->_objectClasses as $val) {
+            if (count($val->sup) > 0) {
+                $this->_resolveInheritance($val, $this->_objectClasses);
+            }
+            foreach ($val->aliases as $alias) {
+                $this->_objectClasses[$alias] = $val;
+            }
+        }
+        ksort($this->_objectClasses, SORT_STRING);
+    }
+
+    /**
+     * Parses an objectClasses value
+     *
+     * @param string $value
+     * @return array
+     */
+    protected function _parseObjectClass($value)
+    {
+        $objectClass = array(
+            'oid'        => null,
+            'name'       => null,
+            'desc'       => null,
+            'obsolete'   => false,
+            'sup'        => array(),
+            'abstract'   => false,
+            'structural' => false,
+            'auxiliary'  => false,
+            'must'       => array(),
+            'may'        => array(),
+            '_string'    => $value,
+            '_parents'   => array());
+
+        $tokens = $this->_tokenizeString($value);
+        $objectClass['oid'] = array_shift($tokens); // first token is the oid
+        $this->_parseLdapSchemaSyntax($objectClass, $tokens);
+
+        $this->_ensureNameAttribute($objectClass);
+
+        return $objectClass;
+    }
+
+    /**
+     * Resolves inheritance in objectClasses and attributes
+     *
+     * @param Zend_Ldap_Node_Schema_Item $node
+     * @param array $repository
+     */
+    protected function _resolveInheritance(Zend_Ldap_Node_Schema_Item $node, array $repository)
+    {
+        $data = $node->getData();
+        $parents = $data['sup'];
+        if ($parents === null || !is_array($parents) || count($parents) < 1) return;
+        foreach ($parents as $parent) {
+            if (!array_key_exists($parent, $repository)) continue;
+            if (!array_key_exists('_parents', $data) || !is_array($data['_parents'])) {
+               $data['_parents'] = array();
+           }
+           $data['_parents'][] = $repository[$parent];
+        }
+        $node->setData($data);
+    }
+
+    /**
+     * Loads the LDAP syntaxes
+     *
+     * @return void
+     */
+    protected function _loadLdapSyntaxes()
+    {
+        $this->_ldapSyntaxes = array();
+        foreach ($this->getAttribute('ldapSyntaxes') as $value) {
+            $val = $this->_parseLdapSyntax($value);
+            $this->_ldapSyntaxes[$val['oid']] = $val;
+        }
+        ksort($this->_ldapSyntaxes, SORT_STRING);
+    }
+
+    /**
+     * Parses an ldapSyntaxes value
+     *
+     * @param string $value
+     * @return array
+     */
+    protected function _parseLdapSyntax($value)
+    {
+        $ldapSyntax = array(
+            'oid'      => null,
+            'desc'     => null,
+            '_string' => $value);
+
+        $tokens = $this->_tokenizeString($value);
+        $ldapSyntax['oid'] = array_shift($tokens); // first token is the oid
+        $this->_parseLdapSchemaSyntax($ldapSyntax, $tokens);
+
+        return $ldapSyntax;
+    }
+
+    /**
+     * Loads the matching rules
+     *
+     * @return void
+     */
+    protected function _loadMatchingRules()
+    {
+        $this->_matchingRules = array();
+        foreach ($this->getAttribute('matchingRules') as $value) {
+            $val = $this->_parseMatchingRule($value);
+            $this->_matchingRules[$val['name']] = $val;
+        }
+        ksort($this->_matchingRules, SORT_STRING);
+    }
+
+    /**
+     * Parses an matchingRules value
+     *
+     * @param string $value
+     * @return array
+     */
+    protected function _parseMatchingRule($value)
+    {
+        $matchingRule = array(
+            'oid'      => null,
+            'name'     => null,
+            'desc'     => null,
+            'obsolete' => false,
+            'syntax'   => null,
+            '_string'  => $value);
+
+        $tokens = $this->_tokenizeString($value);
+        $matchingRule['oid'] = array_shift($tokens); // first token is the oid
+        $this->_parseLdapSchemaSyntax($matchingRule, $tokens);
+
+        $this->_ensureNameAttribute($matchingRule);
+
+        return $matchingRule;
+    }
+
+    /**
+     * Loads the matching rule use
+     *
+     * @return void
+     */
+    protected function _loadMatchingRuleUse()
+    {
+        $this->_matchingRuleUse = array();
+        foreach ($this->getAttribute('matchingRuleUse') as $value) {
+            $val = $this->_parseMatchingRuleUse($value);
+            $this->_matchingRuleUse[$val['name']] = $val;
+        }
+        ksort($this->_matchingRuleUse, SORT_STRING);
+    }
+
+    /**
+     * Parses an matchingRuleUse value
+     *
+     * @param string $value
+     * @return array
+     */
+    protected function _parseMatchingRuleUse($value)
+    {
+        $matchingRuleUse = array(
+            'oid'      => null,
+            'name'     => null,
+            'desc'     => null,
+            'obsolete' => false,
+            'applies'  => array(),
+            '_string'  => $value);
+
+        $tokens = $this->_tokenizeString($value);
+        $matchingRuleUse['oid'] = array_shift($tokens); // first token is the oid
+        $this->_parseLdapSchemaSyntax($matchingRuleUse, $tokens);
+
+        $this->_ensureNameAttribute($matchingRuleUse);
+
+        return $matchingRuleUse;
+    }
+
+    /**
+     * Ensures that a name element is present and that it is single-values.
+     *
+     * @param array $data
+     */
+    protected function _ensureNameAttribute(array &$data)
+    {
+        if (!array_key_exists('name', $data) || empty($data['name'])) {
+            // force a name
+            $data['name'] = $data['oid'];
+        }
+        if (is_array($data['name'])) {
+            // make one name the default and put the other ones into aliases
+            $aliases = $data['name'];
+            $data['name'] = array_shift($aliases);
+            $data['aliases'] = $aliases;
+        } else {
+            $data['aliases'] = array();
+        }
+    }
+
+    /**
+     * Parse the given tokens into a data structure
+     *
+     * @param array $data
+     * @param array $tokens
+     * @return void
+     */
+    protected function _parseLdapSchemaSyntax(array &$data, array $tokens)
+    {
+        // tokens that have no value associated
+        $noValue = array('single-value',
+            'obsolete',
+            'collective',
+            'no-user-modification',
+            'abstract',
+            'structural',
+            'auxiliary');
+        // tokens that can have multiple values
+        $multiValue = array('must', 'may', 'sup');
+
+        while (count($tokens) > 0) {
+            $token = strtolower(array_shift($tokens));
+            if (in_array($token, $noValue)) {
+                $data[$token] = true; // single value token
+            } else {
+                $data[$token] = array_shift($tokens);
+                // this one follows a string or a list if it is multivalued
+                if ($data[$token] == '(') {
+                    // this creates the list of values and cycles through the tokens
+                    // until the end of the list is reached ')'
+                    $data[$token] = array();
+                    while ($tmp = array_shift($tokens)) {
+                        if ($tmp == ')') break;
+                        if ($tmp != '$') {
+                            $data[$token][] = Zend_Ldap_Attribute::convertFromLdapValue($tmp);
+                        }
+                    }
+                } else {
+                    $data[$token] = Zend_Ldap_Attribute::convertFromLdapValue($data[$token]);
+                }
+                // create a array if the value should be multivalued but was not
+                if (in_array($token, $multiValue) && !is_array($data[$token])) {
+                    $data[$token] = array($data[$token]);
+                }
+            }
+        }
+    }
+
+    /**
+    * Tokenizes the given value into an array
+    *
+    * @param string $value string
+    * @return array tokens
+    */
+    protected function _tokenizeString($value)
+    {
+        $tokens = array();
+        $matches = array();
+        // this one is taken from PEAR::Net_LDAP2
+        $pattern = "/\s* (?:([()]) | ([^'\s()]+) | '((?:[^']+|'[^\s)])*)') \s*/x";
+        preg_match_all($pattern, $value, $matches);
+        $cMatches = count($matches[0]);
+        $cPattern = count($matches);
+        for ($i = 0; $i < $cMatches; $i++) {     // number of tokens (full pattern match)
+            for ($j = 1; $j < $cPattern; $j++) { // each subpattern
+                $tok = trim($matches[$j][$i]);
+                if (!empty($tok)) {              // pattern match in this subpattern
+                    $tokens[$i] = $tok;          // this is the token
+                }
+            }
+        }
+        if ($tokens[0] == '(') array_shift($tokens);
+        if ($tokens[count($tokens) - 1] == ')') array_pop($tokens);
+        return $tokens;
+    }
+}

+ 50 - 7
tests/Zend/Ldap/AllTests.php

@@ -29,13 +29,6 @@ if (!defined('PHPUnit_MAIN_METHOD')) {
     define('PHPUnit_MAIN_METHOD', 'Zend_Ldap_AllTests::main');
 }
 
-PHPUnit_Util_Filter::addFileToFilter(__FILE__);
-
-/**
- * @see Zend_Ldap_OfflineTest
- */
-require_once 'Zend/Ldap/OfflineTest.php';
-
 /**
  * @category   Zend
  * @package    Zend_Ldap
@@ -54,7 +47,41 @@ class Zend_Ldap_AllTests
     {
         $suite = new PHPUnit_Framework_TestSuite('Zend Framework - Zend_Ldap');
 
+        /**
+         * @see Zend_Ldap_OfflineTest
+         */
+        require_once 'Zend/Ldap/OfflineTest.php';
         $suite->addTestSuite('Zend_Ldap_OfflineTest');
+        /**
+         * @see Zend_Ldap_AttributeTest
+         */
+        require_once 'Zend/Ldap/AttributeTest.php';
+        $suite->addTestSuite('Zend_Ldap_AttributeTest');
+        /**
+         * @see Zend_Ldap_ConverterTest
+         */
+        require_once 'Zend/Ldap/ConverterTest.php';
+        $suite->addTestSuite('Zend_Ldap_ConverterTest');
+        /**
+         * @see Zend_Ldap_Dn_AllTests
+         */
+        require_once 'Zend/Ldap/Dn/AllTests.php';
+        $suite->addTest(Zend_Ldap_Dn_AllTests::suite());
+        /**
+         * @see Zend_Ldap_FilterTest
+         */
+        require_once 'Zend/Ldap/FilterTest.php';
+        $suite->addTestSuite('Zend_Ldap_FilterTest');
+        /**
+         * @see Zend_Ldap_Node_AllTests
+         */
+        require_once 'Zend/Ldap/Node/AllTests.php';
+        $suite->addTest(Zend_Ldap_Node_AllTests::suite());
+        /**
+         * @see Zend_Ldap_Ldif_AllTests
+         */
+        require_once 'Zend/Ldap/Ldif/AllTests.php';
+        $suite->addTest(Zend_Ldap_Ldif_AllTests::suite());
 
         if (defined('TESTS_ZEND_LDAP_ONLINE_ENABLED')
             && constant('TESTS_ZEND_LDAP_ONLINE_ENABLED')) {
@@ -73,6 +100,22 @@ class Zend_Ldap_AllTests
              */
             require_once 'Zend/Ldap/CanonTest.php';
             $suite->addTestSuite('Zend_Ldap_CanonTest');
+            /**
+             * @see Zend_Ldap_SearchTest
+             */
+            require_once 'Zend/Ldap/SearchTest.php';
+            $suite->addTestSuite('Zend_Ldap_SearchTest');
+            /**
+             * @see Zend_Ldap_CrudTest
+             */
+            require_once 'Zend/Ldap/CrudTest.php';
+            $suite->addTestSuite('Zend_Ldap_CrudTest');
+            /**
+             * @see Zend_Ldap_CopyRenameTest
+             */
+            require_once 'Zend/Ldap/CopyRenameTest.php';
+            $suite->addTestSuite('Zend_Ldap_CopyRenameTest');
+
         } else {
             $suite->addTest(new Zend_Ldap_SkipOnlineTests());
         }

+ 458 - 0
tests/Zend/Ldap/AttributeTest.php

@@ -0,0 +1,458 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Test helper
+ */
+require_once dirname(dirname(dirname(__FILE__))) . DIRECTORY_SEPARATOR . 'TestHelper.php';
+/**
+ * Zend_Ldap_Attribute
+ */
+require_once 'Zend/Ldap/Attribute.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+class Zend_Ldap_AttributeTest extends PHPUnit_Framework_TestCase
+{
+    protected function _assertLocalDateTimeString($timestamp, $value)
+    {
+        $this->assertEquals(date('YmdHisO', $timestamp), $value);
+    }
+
+    protected function _assertUtcDateTimeString($localTimestamp, $value)
+    {
+        $currentOffset = date('Z');
+        $utcTimestamp = $localTimestamp - $currentOffset;
+        $this->assertEquals(date('YmdHis', $utcTimestamp) . 'Z', $value);
+    }
+
+    public function testGetAttributeValue()
+    {
+        $data=array('uid' => array('value'));
+        $value=Zend_Ldap_Attribute::getAttribute($data, 'uid', 0);
+        $this->assertEquals('value', $value);
+    }
+
+    public function testGetNonExistentAttributeValue()
+    {
+        $data=array('uid' => array('value'));
+        $value=Zend_Ldap_Attribute::getAttribute($data, 'uid', 1);
+        $this->assertNull($value);
+    }
+
+    public function testGetNonExistentAttribute()
+    {
+        $data=array('uid' => array('value'));
+        $value=Zend_Ldap_Attribute::getAttribute($data, 'uid2', 0);
+        $this->assertNull($value);
+        $array=Zend_Ldap_Attribute::getAttribute($data, 'uid2');
+        $this->assertType('array', $array);
+        $this->assertEquals(0, count($array));
+    }
+
+    public function testGetAttributeWithWrongIndexType()
+    {
+        $data=array('uid' => array('value'));
+        $value=Zend_Ldap_Attribute::getAttribute($data, 'uid', 'index');
+        $this->assertNull($value);
+        $value=Zend_Ldap_Attribute::getAttribute($data, 'uid', 3.1415);
+        $this->assertNull($value);
+    }
+
+    public function testGetAttributeArray()
+    {
+        $data=array('uid' => array('value'));
+        $value=Zend_Ldap_Attribute::getAttribute($data, 'uid');
+        $this->assertType('array', $value);
+        $this->assertEquals(1, count($value));
+        $this->assertContains('value', $value);
+    }
+
+    public function testSimpleSetAttribute()
+    {
+        $data=array();
+        Zend_Ldap_Attribute::setAttribute($data, 'uid', 'new', false);
+        $this->assertArrayHasKey('uid', $data);
+        $this->assertType('array', $data['uid']);
+        $this->assertEquals(1, count($data['uid']));
+        $this->assertContains('new', $data['uid']);
+    }
+
+    public function testSimpleOverwriteAttribute()
+    {
+        $data=array('uid' => array('old'));
+        Zend_Ldap_Attribute::setAttribute($data, 'uid', 'new', false);
+        $this->assertArrayHasKey('uid', $data);
+        $this->assertType('array', $data['uid']);
+        $this->assertEquals(1, count($data['uid']));
+        $this->assertContains('new', $data['uid']);
+    }
+
+    public function testSimpleAppendAttribute()
+    {
+        $data=array('uid' => array('old'));
+        Zend_Ldap_Attribute::setAttribute($data, 'uid', 'new', true);
+        $this->assertArrayHasKey('uid', $data);
+        $this->assertType('array', $data['uid']);
+        $this->assertEquals(2, count($data['uid']));
+        $this->assertContains('old', $data['uid']);
+        $this->assertContains('new', $data['uid']);
+        $this->assertEquals('old', $data['uid'][0]);
+        $this->assertEquals('new', $data['uid'][1]);
+    }
+
+    public function testBooleanAttributeHandling()
+    {
+        $data=array(
+            'p1_true' => array('TRUE'),
+            'p1_false' => array('FALSE')
+        );
+        Zend_Ldap_Attribute::setAttribute($data, 'p2_true', true);
+        Zend_Ldap_Attribute::setAttribute($data, 'p2_false', false);
+        $this->assertEquals('TRUE', $data['p2_true'][0]);
+        $this->assertEquals('FALSE', $data['p2_false'][0]);
+        $this->assertEquals(true, Zend_Ldap_Attribute::getAttribute($data, 'p1_true', 0));
+        $this->assertEquals(false, Zend_Ldap_Attribute::getAttribute($data, 'p1_false', 0));
+    }
+
+    public function testArraySetAttribute()
+    {
+        $data=array();
+        Zend_Ldap_Attribute::setAttribute($data, 'uid', array('new1', 'new2'), false);
+        $this->assertArrayHasKey('uid', $data);
+        $this->assertType('array', $data['uid']);
+        $this->assertEquals(2, count($data['uid']));
+        $this->assertContains('new1', $data['uid']);
+        $this->assertContains('new2', $data['uid']);
+        $this->assertEquals('new1', $data['uid'][0]);
+        $this->assertEquals('new2', $data['uid'][1]);
+    }
+
+    public function testArrayOverwriteAttribute()
+    {
+        $data=array('uid' => array('old'));
+        Zend_Ldap_Attribute::setAttribute($data, 'uid', array('new1', 'new2'), false);
+        $this->assertArrayHasKey('uid', $data);
+        $this->assertType('array', $data['uid']);
+        $this->assertEquals(2, count($data['uid']));
+        $this->assertContains('new1', $data['uid']);
+        $this->assertContains('new2', $data['uid']);
+        $this->assertEquals('new1', $data['uid'][0]);
+        $this->assertEquals('new2', $data['uid'][1]);
+    }
+
+    public function testArrayAppendAttribute()
+    {
+        $data=array('uid' => array('old'));
+        Zend_Ldap_Attribute::setAttribute($data, 'uid', array('new1', 'new2'), true);
+        $this->assertArrayHasKey('uid', $data);
+        $this->assertType('array', $data['uid']);
+        $this->assertEquals(3, count($data['uid']));
+        $this->assertContains('old', $data['uid']);
+        $this->assertContains('new1', $data['uid']);
+        $this->assertContains('new2', $data['uid']);
+        $this->assertEquals('old', $data['uid'][0]);
+        $this->assertEquals('new1', $data['uid'][1]);
+        $this->assertEquals('new2', $data['uid'][2]);
+    }
+
+    public function testSHAPasswordGeneration()
+    {
+        $data=array();
+        Zend_Ldap_Attribute::setPassword($data, 'pa$$w0rd', Zend_Ldap_Attribute::PASSWORD_HASH_SHA);
+        $password=Zend_Ldap_Attribute::getAttribute($data, 'userPassword', 0);
+        $this->assertEquals('{SHA}vi3X+3ptD4ulrdErXo+3W72mRyE=', $password);
+    }
+
+    public function testMD5PasswordGeneration()
+    {
+        $data=array();
+        Zend_Ldap_Attribute::setPassword($data, 'pa$$w0rd', Zend_Ldap_Attribute::PASSWORD_HASH_MD5);
+        $password=Zend_Ldap_Attribute::getAttribute($data, 'userPassword', 0);
+        $this->assertEquals('{MD5}bJuLJ96h3bhF+WqiVnxnVA==', $password);
+    }
+
+    public function testSetAttributeWithObject()
+    {
+        $data=array();
+        $object=new stdClass();
+        $object->a=1;
+        $object->b=1.23;
+        $object->c='string';
+        Zend_Ldap_Attribute::setAttribute($data, 'object', $object);
+        $this->assertEquals(serialize($object), $data['object'][0]);
+    }
+
+    public function testSetAttributeWithFilestream()
+    {
+        $data=array();
+        $stream=fopen(dirname(__FILE__) . '/_files/AttributeTest.input.txt', 'r');
+        Zend_Ldap_Attribute::setAttribute($data, 'file', $stream);
+        fclose($stream);
+        $this->assertEquals('String from file', $data['file'][0]);
+    }
+
+    public function testSetDateTimeValueLocal()
+    {
+        $ts=mktime(12, 30, 30, 6, 25, 2008);
+        $data=array();
+        Zend_Ldap_Attribute::setDateTimeAttribute($data, 'ts', $ts, false);
+        $this->_assertLocalDateTimeString($ts, $data['ts'][0]);
+    }
+
+    public function testSetDateTimeValueUtc()
+    {
+        $ts=mktime(12, 30, 30, 6, 25, 2008);
+        $data=array();
+        Zend_Ldap_Attribute::setDateTimeAttribute($data, 'ts', $ts, true);
+        $this->_assertUtcDateTimeString($ts, $data['ts'][0]);
+    }
+
+    public function testSetDateTimeValueLocalArray()
+    {
+        $ts=array();
+        $ts[]=mktime(12, 30, 30, 6, 25, 2008);
+        $ts[]=mktime(1, 25, 30, 1, 2, 2008);
+        $data=array();
+        Zend_Ldap_Attribute::setDateTimeAttribute($data, 'ts', $ts, false);
+        $this->_assertLocalDateTimeString($ts[0], $data['ts'][0]);
+        $this->_assertLocalDateTimeString($ts[1], $data['ts'][1]);
+    }
+
+    public function testSetDateTimeValueIllegal()
+    {
+        $ts='dummy';
+        $data=array();
+        Zend_Ldap_Attribute::setDateTimeAttribute($data, 'ts', $ts, false);
+        $this->assertEquals(0, count($data['ts']));
+    }
+
+    public function testGetDateTimeValueFromLocal()
+    {
+        $ts=mktime(12, 30, 30, 6, 25, 2008);
+        $data=array();
+        Zend_Ldap_Attribute::setDateTimeAttribute($data, 'ts', $ts, false);
+        $this->_assertLocalDateTimeString($ts, $data['ts'][0]);
+        $retTs=Zend_Ldap_Attribute::getDateTimeAttribute($data, 'ts', 0);
+        $this->assertEquals($ts, $retTs);
+    }
+
+    public function testGetDateTimeValueFromUtc()
+    {
+        $ts=mktime(12, 30, 30, 6, 25, 2008);
+        $data=array();
+        Zend_Ldap_Attribute::setDateTimeAttribute($data, 'ts', $ts, true);
+        $this->_assertUtcDateTimeString($ts, $data['ts'][0]);
+        $retTs=Zend_Ldap_Attribute::getDateTimeAttribute($data, 'ts', 0);
+        $this->assertEquals($ts, $retTs);
+    }
+
+    public function testGetDateTimeValueFromArray()
+    {
+        $ts=array();
+        $ts[]=mktime(12, 30, 30, 6, 25, 2008);
+        $ts[]=mktime(1, 25, 30, 1, 2, 2008);
+        $data=array();
+        Zend_Ldap_Attribute::setDateTimeAttribute($data, 'ts', $ts, false);
+        $this->_assertLocalDateTimeString($ts[0], $data['ts'][0]);
+        $this->_assertLocalDateTimeString($ts[1], $data['ts'][1]);
+        $retTs=Zend_Ldap_Attribute::getDateTimeAttribute($data, 'ts');
+        $this->assertEquals($ts[0], $retTs[0]);
+        $this->assertEquals($ts[1], $retTs[1]);
+    }
+
+    public function testGetDateTimeValueIllegal()
+    {
+        $data=array('ts' => array('dummy'));
+        $retTs=Zend_Ldap_Attribute::getDateTimeAttribute($data, 'ts', 0);
+        $this->assertNull($retTs);
+    }
+
+    public function testGetDateTimeValueNegativeOffet()
+    {
+        $data=array('ts' => array('20080612143045-0700'));
+        $retTs=Zend_Ldap_Attribute::getDateTimeAttribute($data, 'ts', 0);
+        $tsCompare=gmmktime(21, 30, 45, 6, 12, 2008);
+        $this->assertEquals($tsCompare, $retTs);
+    }
+
+    public function testGetDateTimeValueNegativeOffet2()
+    {
+        $data=array('ts' => array('20080612143045-0715'));
+        $retTs=Zend_Ldap_Attribute::getDateTimeAttribute($data, 'ts', 0);
+        $tsCompare=gmmktime(21, 45, 45, 6, 12, 2008);
+        $this->assertEquals($tsCompare, $retTs);
+    }
+
+    public function testRemoveAttributeValueSimple()
+    {
+        $data=array('test' => array('value1', 'value2', 'value3', 'value3'));
+        Zend_Ldap_Attribute::removeFromAttribute($data, 'test', 'value2');
+        $this->assertArrayHasKey('test', $data);
+        $this->assertType('array', $data['test']);
+        $this->assertEquals(3, count($data['test']));
+        $this->assertContains('value1', $data['test']);
+        $this->assertContains('value3', $data['test']);
+        $this->assertNotContains('value2', $data['test']);
+    }
+
+    public function testRemoveAttributeValueArray()
+    {
+        $data=array('test' => array('value1', 'value2', 'value3', 'value3'));
+        Zend_Ldap_Attribute::removeFromAttribute($data, 'test', array('value1', 'value2'));
+        $this->assertArrayHasKey('test', $data);
+        $this->assertType('array', $data['test']);
+        $this->assertEquals(2, count($data['test']));
+        $this->assertContains('value3', $data['test']);
+        $this->assertNotContains('value1', $data['test']);
+        $this->assertNotContains('value2', $data['test']);
+    }
+
+    public function testRemoveAttributeMultipleValueSimple()
+    {
+        $data=array('test' => array('value1', 'value2', 'value3', 'value3'));
+        Zend_Ldap_Attribute::removeFromAttribute($data, 'test', 'value3');
+        $this->assertArrayHasKey('test', $data);
+        $this->assertType('array', $data['test']);
+        $this->assertEquals(2, count($data['test']));
+        $this->assertContains('value1', $data['test']);
+        $this->assertContains('value2', $data['test']);
+        $this->assertNotContains('value3', $data['test']);
+    }
+
+    public function testRemoveAttributeMultipleValueArray()
+    {
+        $data=array('test' => array('value1', 'value2', 'value3', 'value3'));
+        Zend_Ldap_Attribute::removeFromAttribute($data, 'test', array('value1', 'value3'));
+        $this->assertArrayHasKey('test', $data);
+        $this->assertType('array', $data['test']);
+        $this->assertEquals(1, count($data['test']));
+        $this->assertContains('value2', $data['test']);
+        $this->assertNotContains('value1', $data['test']);
+        $this->assertNotContains('value3', $data['test']);
+    }
+
+    public function testRemoveAttributeValueBoolean()
+    {
+        $data=array('test' => array('TRUE', 'FALSE', 'TRUE', 'FALSE'));
+        Zend_Ldap_Attribute::removeFromAttribute($data, 'test', false);
+        $this->assertArrayHasKey('test', $data);
+        $this->assertType('array', $data['test']);
+        $this->assertEquals(2, count($data['test']));
+        $this->assertContains('TRUE', $data['test']);
+        $this->assertNotContains('FALSE', $data['test']);
+    }
+
+    public function testRemoveAttributeValueInteger()
+    {
+        $data=array('test' => array('1', '2', '3', '4'));
+        Zend_Ldap_Attribute::removeFromAttribute($data, 'test', array(2, 4));
+        $this->assertArrayHasKey('test', $data);
+        $this->assertType('array', $data['test']);
+        $this->assertEquals(2, count($data['test']));
+        $this->assertContains('1', $data['test']);
+        $this->assertContains('3', $data['test']);
+        $this->assertNotContains('2', $data['test']);
+        $this->assertNotContains('4', $data['test']);
+    }
+
+    public function testConvertFromLdapValue()
+    {
+        $this->assertEquals(true, Zend_Ldap_Attribute::convertFromLdapValue('TRUE'));
+        $this->assertEquals(false, Zend_Ldap_Attribute::convertFromLdapValue('FALSE'));
+    }
+
+    public function testConvertToLdapValue()
+    {
+        $this->assertEquals('string', Zend_Ldap_Attribute::convertToLdapValue('string'));
+        $this->assertEquals('1', Zend_Ldap_Attribute::convertToLdapValue(1));
+        $this->assertEquals('TRUE', Zend_Ldap_Attribute::convertToLdapValue(true));
+    }
+
+    public function testConvertFromLdapDateTimeValue()
+    {
+        $ldap='20080625123030+0200';
+        $this->assertEquals(gmmktime(10, 30, 30, 6, 25, 2008),
+            Zend_Ldap_Attribute::convertFromLdapDateTimeValue($ldap));
+    }
+
+    public function testConvertToLdapDateTimeValue()
+    {
+        $ts=mktime(12, 30, 30, 6, 25, 2008);
+        $this->_assertLocalDateTimeString($ts, Zend_Ldap_Attribute::convertToLdapDateTimeValue($ts));
+    }
+
+    public function testRemoveDuplicates()
+    {
+        $data=array(
+            'strings1' => array('value1', 'value2', 'value2', 'value3'),
+            'strings2' => array('value1', 'value2', 'value3', 'value4'),
+            'boolean1' => array('TRUE', 'TRUE', 'TRUE', 'TRUE'),
+            'boolean2' => array('TRUE', 'FALSE', 'TRUE', 'FALSE'),
+        );
+        $expected=array(
+            'strings1' => array('value1', 'value2', 'value3'),
+            'strings2' => array('value1', 'value2', 'value3', 'value4'),
+            'boolean1' => array('TRUE'),
+            'boolean2' => array('TRUE', 'FALSE'),
+        );
+        Zend_Ldap_Attribute::removeDuplicatesFromAttribute($data, 'strings1');
+        Zend_Ldap_Attribute::removeDuplicatesFromAttribute($data, 'strings2');
+        Zend_Ldap_Attribute::removeDuplicatesFromAttribute($data, 'boolean1');
+        Zend_Ldap_Attribute::removeDuplicatesFromAttribute($data, 'boolean2');
+        $this->assertEquals($expected, $data);
+    }
+
+    public function testHasValue()
+    {
+        $data=array(
+            'strings1' => array('value1', 'value2', 'value2', 'value3'),
+            'strings2' => array('value1', 'value2', 'value3', 'value4'),
+            'boolean1' => array('TRUE', 'TRUE', 'TRUE', 'TRUE'),
+            'boolean2' => array('TRUE', 'FALSE', 'TRUE', 'FALSE'),
+        );
+
+        $this->assertTrue(Zend_Ldap_Attribute::attributeHasValue($data, 'strings1', 'value1'));
+        $this->assertFalse(Zend_Ldap_Attribute::attributeHasValue($data, 'strings1', 'value4'));
+        $this->assertTrue(Zend_Ldap_Attribute::attributeHasValue($data, 'boolean1', true));
+        $this->assertFalse(Zend_Ldap_Attribute::attributeHasValue($data, 'boolean1', false));
+
+        $this->assertTrue(Zend_Ldap_Attribute::attributeHasValue($data, 'strings1',
+            array('value1', 'value2')));
+        $this->assertTrue(Zend_Ldap_Attribute::attributeHasValue($data, 'strings1',
+            array('value1', 'value2', 'value3')));
+        $this->assertFalse(Zend_Ldap_Attribute::attributeHasValue($data, 'strings1',
+            array('value1', 'value2', 'value3', 'value4')));
+        $this->assertTrue(Zend_Ldap_Attribute::attributeHasValue($data, 'strings2',
+            array('value1', 'value2', 'value3', 'value4')));
+
+        $this->assertTrue(Zend_Ldap_Attribute::attributeHasValue($data, 'boolean2',
+            array(true, false)));
+        $this->assertFalse(Zend_Ldap_Attribute::attributeHasValue($data, 'boolean1',
+            array(true, false)));
+    }
+}

+ 64 - 17
tests/Zend/Ldap/BindTest.php

@@ -47,7 +47,7 @@ class Zend_Ldap_BindTest extends PHPUnit_Framework_TestCase
 {
     protected $_options = null;
     protected $_principalName = TESTS_ZEND_LDAP_PRINCIPAL_NAME;
-    protected $_altUsername = TESTS_ZEND_LDAP_PRINCIPAL_NAME;
+    protected $_altUsername = TESTS_ZEND_LDAP_ALT_USERNAME;
     protected $_bindRequiresDn = false;
 
     public function setUp()
@@ -58,7 +58,7 @@ class Zend_Ldap_BindTest extends PHPUnit_Framework_TestCase
             'password' => TESTS_ZEND_LDAP_PASSWORD,
             'baseDn' => TESTS_ZEND_LDAP_BASE_DN,
         );
-        if (defined('TESTS_ZEND_LDAP_PORT') && TESTS_ZEND_LDAP_PORT != 389)
+        if (defined('TESTS_ZEND_LDAP_PORT'))
             $this->_options['port'] = TESTS_ZEND_LDAP_PORT;
         if (defined('TESTS_ZEND_LDAP_USE_START_TLS'))
             $this->_options['useStartTls'] = TESTS_ZEND_LDAP_USE_START_TLS;
@@ -66,6 +66,12 @@ class Zend_Ldap_BindTest extends PHPUnit_Framework_TestCase
             $this->_options['useSsl'] = TESTS_ZEND_LDAP_USE_SSL;
         if (defined('TESTS_ZEND_LDAP_BIND_REQUIRES_DN'))
             $this->_options['bindRequiresDn'] = TESTS_ZEND_LDAP_BIND_REQUIRES_DN;
+        if (defined('TESTS_ZEND_LDAP_ACCOUNT_FILTER_FORMAT'))
+            $this->_options['accountFilterFormat'] = TESTS_ZEND_LDAP_ACCOUNT_FILTER_FORMAT;
+        if (defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME'))
+            $this->_options['accountDomainName'] = TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME;
+        if (defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT'))
+            $this->_options['accountDomainNameShort'] = TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT;
         if (defined('TESTS_ZEND_LDAP_ALT_USERNAME'))
             $this->_altUsername = TESTS_ZEND_LDAP_ALT_USERNAME;
 
@@ -113,8 +119,9 @@ class Zend_Ldap_BindTest extends PHPUnit_Framework_TestCase
     public function testNoDomainNameBind()
     {
         $options = $this->_options;
-        unset($options['baseDn']);
+        unset($options['accountDomainName']);
         $options['bindRequiresDn'] = false;
+        $options['accountCanonicalForm'] = Zend_Ldap::ACCTNAME_FORM_PRINCIPAL;
 
         $ldap = new Zend_Ldap($options);
         try {
@@ -128,11 +135,13 @@ class Zend_Ldap_BindTest extends PHPUnit_Framework_TestCase
     {
         $ldap = new Zend_Ldap($this->_options);
         $ldap->bind();
+        $this->assertNotNull($ldap->getResource());
     }
     public function testConnectBind()
     {
         $ldap = new Zend_Ldap($this->_options);
         $ldap->connect()->bind();
+        $this->assertNotNull($ldap->getResource());
     }
     public function testExplicitParamsBind()
     {
@@ -145,35 +154,26 @@ class Zend_Ldap_BindTest extends PHPUnit_Framework_TestCase
 
         $ldap = new Zend_Ldap($options);
         $ldap->bind($username, $password);
+        $this->assertNotNull($ldap->getResource());
     }
     public function testRequiresDnBind()
     {
         $options = $this->_options;
 
-        /* Fixup filter since bindRequiresDn is used to determine default accountFilterFormat
-         */
-        if (!isset($options['accountFilterFormat']) && $this->_bindRequiresDn === false)
-            $options['accountFilterFormat'] = '(&(objectClass=user)(sAMAccountName=%s))';
-
         $options['bindRequiresDn'] = true;
 
         $ldap = new Zend_Ldap($options);
         try {
             $ldap->bind($this->_altUsername, 'invalid');
+            $this->fail('Expected exception not thrown');
         } catch (Zend_Ldap_Exception $zle) {
-			$message = str_replace("\n", " ", $zle->getMessage());
-            $this->assertContains('Invalid credentials', $message);
+            $this->assertContains('Invalid credentials', $zle->getMessage());
         }
     }
     public function testRequiresDnWithoutDnBind()
     {
         $options = $this->_options;
 
-        /* Fixup filter since bindRequiresDn is used to determine default accountFilterFormat
-         */
-        if (!isset($options['accountFilterFormat']) && !$this->_bindRequiresDn)
-            $options['accountFilterFormat'] = '(&(objectClass=user)(sAMAccountName=%s))';
-
         $options['bindRequiresDn'] = true;
 
         unset($options['username']);
@@ -181,10 +181,57 @@ class Zend_Ldap_BindTest extends PHPUnit_Framework_TestCase
         $ldap = new Zend_Ldap($options);
         try {
             $ldap->bind($this->_principalName);
+            $this->fail('Expected exception not thrown');
         } catch (Zend_Ldap_Exception $zle) {
-			/* Note that if your server actually allows anonymous binds this test will fail.
-			 */
+            /* Note that if your server actually allows anonymous binds this test will fail.
+             */
             $this->assertContains('Failed to retrieve DN', $zle->getMessage());
         }
     }
+
+    public function testBindWithEmptyPassword()
+    {
+        $options = $this->_options;
+        $options['allowEmptyPassword'] = false;
+        $ldap = new Zend_Ldap($options);
+        try {
+            $ldap->bind($this->_altUsername, '');
+            $this->fail('Expected exception for empty password');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertContains('Empty password not allowed - see allowEmptyPassword option.',
+                $zle->getMessage());
+        }
+
+        $options['allowEmptyPassword'] = true;
+        $ldap = new Zend_Ldap($options);
+        try {
+            $ldap->bind($this->_altUsername, '');
+        } catch (Zend_Ldap_Exception $zle) {
+            if ($zle->getMessage() ===
+                    'Empty password not allowed - see allowEmptyPassword option.') {
+                $this->fail('Exception for empty password');
+            } else {
+                $message = $zle->getMessage();
+                $this->assertTrue(strstr($message, 'Invalid credentials') ||
+                    strstr($message, 'Server is unwilling to perform'));
+                return;
+            }
+        }
+        $this->assertNotNull($ldap->getResource());
+    }
+
+    public function testBindWithoutDnUsernameAndDnRequired()
+    {
+        $options = $this->_options;
+        $options['username'] = TESTS_ZEND_LDAP_ALT_USERNAME;
+        $options['bindRequiresDn'] = true;
+        $ldap = new Zend_Ldap($options);
+        try {
+            $ldap->bind();
+            $this->fail('Expected exception for empty password');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertContains('Binding requires username in DN form',
+                $zle->getMessage());
+        }
+    }
 }

+ 271 - 30
tests/Zend/Ldap/CanonTest.php

@@ -46,8 +46,6 @@ require_once 'Zend/Ldap.php';
 class Zend_Ldap_CanonTest extends PHPUnit_Framework_TestCase
 {
     protected $_options = null;
-    protected $_principalName = TESTS_ZEND_LDAP_PRINCIPAL_NAME;
-    protected $_names = array();
 
     public function setUp()
     {
@@ -57,72 +55,315 @@ class Zend_Ldap_CanonTest extends PHPUnit_Framework_TestCase
             'password' => TESTS_ZEND_LDAP_PASSWORD,
             'baseDn' => TESTS_ZEND_LDAP_BASE_DN,
         );
-        if (defined('TESTS_ZEND_LDAP_PORT') && TESTS_ZEND_LDAP_PORT != 389)
+        if (defined('TESTS_ZEND_LDAP_PORT'))
             $this->_options['port'] = TESTS_ZEND_LDAP_PORT;
+        if (defined('TESTS_ZEND_LDAP_USE_START_TLS'))
+            $this->_options['useStartTls'] = TESTS_ZEND_LDAP_USE_START_TLS;
         if (defined('TESTS_ZEND_LDAP_USE_SSL'))
             $this->_options['useSsl'] = TESTS_ZEND_LDAP_USE_SSL;
         if (defined('TESTS_ZEND_LDAP_BIND_REQUIRES_DN'))
             $this->_options['bindRequiresDn'] = TESTS_ZEND_LDAP_BIND_REQUIRES_DN;
+        if (defined('TESTS_ZEND_LDAP_ACCOUNT_FILTER_FORMAT'))
+            $this->_options['accountFilterFormat'] = TESTS_ZEND_LDAP_ACCOUNT_FILTER_FORMAT;
         if (defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME'))
             $this->_options['accountDomainName'] = TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME;
         if (defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT'))
             $this->_options['accountDomainNameShort'] = TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT;
+    }
+
+    public function testPlainCanon()
+    {
+        $ldap = new Zend_Ldap($this->_options);
+        /* This test tries to canonicalize each name (uname, uname@example.com,
+         * EXAMPLE\uname) to each of the 3 forms (username, principal and backslash)
+         * for a total of canonicalizations.
+         */
         if (defined('TESTS_ZEND_LDAP_ALT_USERNAME')) {
-            $this->_names[Zend_Ldap::ACCTNAME_FORM_USERNAME] = TESTS_ZEND_LDAP_ALT_USERNAME;
+            $names[Zend_Ldap::ACCTNAME_FORM_USERNAME] = TESTS_ZEND_LDAP_ALT_USERNAME;
             if (defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME')) {
-                $this->_names[Zend_Ldap::ACCTNAME_FORM_PRINCIPAL] =
+                $names[Zend_Ldap::ACCTNAME_FORM_PRINCIPAL] =
                     TESTS_ZEND_LDAP_ALT_USERNAME . '@' . TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME;
             }
             if (defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT')) {
-                $this->_names[Zend_Ldap::ACCTNAME_FORM_BACKSLASH] =
+                $names[Zend_Ldap::ACCTNAME_FORM_BACKSLASH] =
                     TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT . '\\' . TESTS_ZEND_LDAP_ALT_USERNAME;
             }
         }
-    }
-
-    public function testPlainCanon()
-    {
-        $ldap = new Zend_Ldap($this->_options);
 
-        /* This test tries to canonicalize each name (uname, uname@example.com,
-         * EXAMPLE\uname) to each of the 3 forms (username, principal and backslash)
-         * for a total of canonicalizations.
-         */
-        foreach ($this->_names as $_form => $name) {
-            foreach ($this->_names as $form => $_name) {
+        foreach ($names as $_form => $name) {
+            foreach ($names as $form => $_name) {
                 $ret = $ldap->getCanonicalAccountName($name, $form);
-                $this->assertTrue($ret === $this->_names[$form]);
+                $this->assertEquals($names[$form], $ret);
             }
         }
     }
+
     public function testInvalidAccountCanon()
     {
         $ldap = new Zend_Ldap($this->_options);
         try {
             $ldap->bind('invalid', 'invalid');
+            $this->fail('Expected exception not thrown');
         } catch (Zend_Ldap_Exception $zle) {
             $msg = $zle->getMessage();
-            $this->assertTrue(strstr($msg, 'Invalid credentials') || strstr($msg, 'No such object'));
+            $this->assertTrue(strstr($msg, 'Invalid credentials') ||
+                strstr($msg, 'No such object') ||
+                strstr($msg, 'No object found'));
         }
     }
+
     public function testDnCanon()
     {
-		$ldap = new Zend_Ldap($this->_options);
-		$name = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_DN, Zend_Ldap::ACCTNAME_FORM_DN);
+        $ldap = new Zend_Ldap($this->_options);
+        $name = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME, Zend_Ldap::ACCTNAME_FORM_DN);
+        $this->assertEquals(TESTS_ZEND_LDAP_ALT_DN, $name);
     }
+
     public function testMismatchDomainBind()
     {
         $ldap = new Zend_Ldap($this->_options);
-		try {
-			$ldap->bind('BOGUS\\doesntmatter', 'doesntmatter');
-		} catch (Zend_Ldap_Exception $zle) {
-			$this->assertTrue($zle->getCode() == Zend_Ldap_Exception::LDAP_X_DOMAIN_MISMATCH);
-		}
+        try {
+            $ldap->bind('BOGUS\\doesntmatter', 'doesntmatter');
+            $this->fail('Expected exception not thrown');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertTrue($zle->getCode() == Zend_Ldap_Exception::LDAP_X_DOMAIN_MISMATCH);
+        }
     }
-    public function testBindCanon()
+
+    public function testAccountCanonization()
     {
-        /**
-         * @todo test accountCanonicalForm option
-         */
+        $options = $this->_options;
+        $ldap = new Zend_Ldap($options);
+
+        $canonDn = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME,
+            Zend_Ldap::ACCTNAME_FORM_DN);
+        $this->assertEquals(TESTS_ZEND_LDAP_ALT_DN, $canonDn);
+        $canonUsername = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME,
+            Zend_Ldap::ACCTNAME_FORM_USERNAME);
+        $this->assertEquals(TESTS_ZEND_LDAP_ALT_USERNAME, $canonUsername);
+        $canonBackslash = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME,
+            Zend_Ldap::ACCTNAME_FORM_BACKSLASH);
+        $this->assertEquals(
+            TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT . '\\' . TESTS_ZEND_LDAP_ALT_USERNAME,
+            $canonBackslash);
+        $canonPrincipal = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME,
+            Zend_Ldap::ACCTNAME_FORM_PRINCIPAL);
+        $this->assertEquals(
+            TESTS_ZEND_LDAP_ALT_USERNAME . '@' . TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME,
+            $canonPrincipal);
+
+        $options['accountCanonicalForm'] = Zend_Ldap::ACCTNAME_FORM_USERNAME;
+        $ldap->setOptions($options);
+        $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME);
+        $this->assertEquals(TESTS_ZEND_LDAP_ALT_USERNAME, $canon);
+
+        $options['accountCanonicalForm'] = Zend_Ldap::ACCTNAME_FORM_BACKSLASH;
+        $ldap->setOptions($options);
+        $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME);
+        $this->assertEquals(
+            TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT . '\\' . TESTS_ZEND_LDAP_ALT_USERNAME, $canon);
+
+        $options['accountCanonicalForm'] = Zend_Ldap::ACCTNAME_FORM_PRINCIPAL;
+        $ldap->setOptions($options);
+        $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME);
+        $this->assertEquals(
+            TESTS_ZEND_LDAP_ALT_USERNAME . '@' . TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME, $canon);
+
+        unset($options['accountCanonicalForm']);
+
+        unset($options['accountDomainName']);
+        $ldap->setOptions($options);
+        $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME);
+        $this->assertEquals(
+            TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT . '\\' . TESTS_ZEND_LDAP_ALT_USERNAME, $canon);
+
+        unset($options['accountDomainNameShort']);
+        $ldap->setOptions($options);
+        $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME);
+        $this->assertEquals(TESTS_ZEND_LDAP_ALT_USERNAME, $canon);
+
+        $options['accountDomainName'] = TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME;
+        $ldap->setOptions($options);
+        $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME);
+        $this->assertEquals(
+            TESTS_ZEND_LDAP_ALT_USERNAME . '@' . TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME, $canon);
+    }
+
+    public function testDefaultAccountFilterFormat()
+    {
+        $options = $this->_options;
+
+        unset($options['accountFilterFormat']);
+        $options['bindRequiresDn'] = true;
+        $ldap = new Zend_Ldap($options);
+        try {
+            $canon = $ldap->getCanonicalAccountName('invalid', Zend_Ldap::ACCTNAME_FORM_DN);
+            $this->fail('Expected exception not thrown');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertContains('(&(objectClass=posixAccount)(uid=invalid))', $zle->getMessage());
+        }
+
+        $options['bindRequiresDn'] = false;
+        $ldap = new Zend_Ldap($options);
+        try {
+            $canon = $ldap->getCanonicalAccountName('invalid', Zend_Ldap::ACCTNAME_FORM_DN);
+            $this->fail('Expected exception not thrown');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertContains('(&(objectClass=user)(sAMAccountName=invalid))', $zle->getMessage());
+        }
+    }
+
+    public function testPossibleAuthority()
+    {
+        $options = $this->_options;
+        $ldap = new Zend_Ldap($options);
+        try {
+            $canon = $ldap->getCanonicalAccountName('invalid\invalid',
+                Zend_Ldap::ACCTNAME_FORM_USERNAME);
+            $this->fail('Expected exception not thrown');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertContains('Binding domain is not an authority for user: invalid\invalid',
+                $zle->getMessage());
+        }
+        try {
+            $canon = $ldap->getCanonicalAccountName('invalid@invalid.tld',
+                Zend_Ldap::ACCTNAME_FORM_USERNAME);
+            $this->fail('Expected exception not thrown');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertContains('Binding domain is not an authority for user: invalid@invalid.tld',
+                $zle->getMessage());
+        }
+
+        unset($options['accountDomainName']);
+        $ldap = new Zend_Ldap($options);
+        $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT . '\invalid',
+            Zend_Ldap::ACCTNAME_FORM_USERNAME);
+        $this->assertEquals('invalid', $canon);
+        try {
+            $canon = $ldap->getCanonicalAccountName('invalid@' . TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME,
+                Zend_Ldap::ACCTNAME_FORM_USERNAME);
+            $this->fail('Expected exception not thrown');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertContains('Binding domain is not an authority for user: invalid@' .
+                TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME,
+                $zle->getMessage());
+        }
+
+        unset($options['accountDomainNameShort']);
+        $options['accountDomainName'] = TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME;
+        $ldap = new Zend_Ldap($options);
+        try {
+            $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT . '\invalid',
+                Zend_Ldap::ACCTNAME_FORM_USERNAME);
+            $this->fail('Expected exception not thrown');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertContains('Binding domain is not an authority for user: ' .
+                TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT . '\invalid',
+                $zle->getMessage());
+        }
+
+        $canon = $ldap->getCanonicalAccountName('invalid@' . TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME,
+            Zend_Ldap::ACCTNAME_FORM_USERNAME);
+        $this->assertEquals('invalid', $canon);
+
+        unset($options['accountDomainName']);
+        $ldap = new Zend_Ldap($options);
+        $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT . '\invalid',
+            Zend_Ldap::ACCTNAME_FORM_USERNAME);
+        $this->assertEquals('invalid', $canon);
+        $canon = $ldap->getCanonicalAccountName('invalid@' . TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME,
+            Zend_Ldap::ACCTNAME_FORM_USERNAME);
+        $this->assertEquals('invalid', $canon);
+    }
+
+    public function testInvalidAccountName()
+    {
+        $options = $this->_options;
+        $ldap = new Zend_Ldap($options);
+
+        try {
+            $canon = $ldap->getCanonicalAccountName('0@' . TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME,
+                Zend_Ldap::ACCTNAME_FORM_USERNAME);
+            $this->fail('Expected exception not thrown');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertContains('Invalid account name syntax: 0@' .
+                TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME,
+                $zle->getMessage());
+        }
+
+        try {
+            $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT . '\\0',
+                Zend_Ldap::ACCTNAME_FORM_USERNAME);
+            $this->fail('Expected exception not thrown');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertContains('Invalid account name syntax: ' .
+                TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT . '\\0',
+                $zle->getMessage());
+        }
+    }
+
+    public function testGetUnknownCanonicalForm()
+    {
+        $options = $this->_options;
+        $ldap = new Zend_Ldap($options);
+
+        try {
+            $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME, 99);
+            $this->fail('Expected exception not thrown');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertContains('Unknown canonical name form: 99',
+                $zle->getMessage());
+        }
+    }
+
+    public function testGetUnavailableCanoncialForm()
+    {
+        $options = $this->_options;
+        unset($options['accountDomainName']);
+        $ldap = new Zend_Ldap($options);
+        try {
+            $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME,
+                Zend_Ldap::ACCTNAME_FORM_PRINCIPAL);
+            $this->fail('Expected exception not thrown');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertContains('Option required: accountDomainName',
+                $zle->getMessage());
+        }
+
+        unset($options['accountDomainNameShort']);
+        $ldap = new Zend_Ldap($options);
+        try {
+            $canon = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_USERNAME,
+                Zend_Ldap::ACCTNAME_FORM_BACKSLASH);
+            $this->fail('Expected exception not thrown');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertContains('Option required: accountDomainNameShort',
+                $zle->getMessage());
+        }
+    }
+
+    public function testSplittingOption()
+    {
+        $options = $this->_options;
+        unset($options['accountDomainName']);
+        unset($options['accountDomainNameShort']);
+        $options['tryUsernameSplit'] = true;
+        $ldap = new Zend_Ldap($options);
+        $this->assertEquals('username', $ldap->getCanonicalAccountName('username@example.com',
+            Zend_Ldap::ACCTNAME_FORM_USERNAME));
+        $this->assertEquals('username', $ldap->getCanonicalAccountName('EXAMPLE\username',
+            Zend_Ldap::ACCTNAME_FORM_USERNAME));
+        $this->assertEquals('username', $ldap->getCanonicalAccountName('username',
+            Zend_Ldap::ACCTNAME_FORM_USERNAME));
+
+        $options['tryUsernameSplit'] = false;
+        $ldap = new Zend_Ldap($options);
+        $this->assertEquals('username@example.com',
+            $ldap->getCanonicalAccountName('username@example.com', Zend_Ldap::ACCTNAME_FORM_USERNAME));
+        $this->assertEquals('example\username', $ldap->getCanonicalAccountName('EXAMPLE\username',
+            Zend_Ldap::ACCTNAME_FORM_USERNAME));
+        $this->assertEquals('username', $ldap->getCanonicalAccountName('username',
+            Zend_Ldap::ACCTNAME_FORM_USERNAME));
+
     }
 }

+ 18 - 0
tests/Zend/Ldap/ConnectTest.php

@@ -175,4 +175,22 @@ class Zend_Ldap_ConnectTest extends PHPUnit_Framework_TestCase
             }
         }
     }
+
+    public function testGetErrorCode()
+    {
+        $ldap = new Zend_Ldap($this->_options);
+        try {
+            // Connect doesn't actually try to connect until bind is called
+            // but if we get 'Invalid credentials' then we know the connect
+            // succeeded.
+            $ldap->connect()->bind('CN=ignored,DC=example,DC=com', 'ignored');
+            $this->fail('Expected exception for invalid username');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertContains('Invalid credentials', $zle->getMessage());
+
+            $this->assertEquals(0x31, $zle->getCode());
+            $this->assertEquals(0x0, Zend_Ldap_Exception::getLdapCode($ldap));
+            $this->assertEquals(0x0, Zend_Ldap_Exception::getLdapCode(null));
+        }
+    }
 }

+ 65 - 0
tests/Zend/Ldap/ConverterTest.php

@@ -0,0 +1,65 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Test helper
+ */
+require_once dirname(dirname(dirname(__FILE__))) . DIRECTORY_SEPARATOR . 'TestHelper.php';
+/**
+ * Zend_Ldap_Converter
+ */
+require_once 'Zend/Ldap/Converter.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+class Zend_Ldap_ConverterTest extends PHPUnit_Framework_TestCase 
+{
+    public function testAsc2hex32() 
+    {
+        $expected='\00\01\02\03\04\05\06\07\08\09\0a\0b\0c\0d\0e\0f\10\11\12\13\14\15\16\17\18\19' . 
+            '\1a\1b\1c\1d\1e\1f !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`' . 
+            'abcdefghijklmnopqrstuvwxyz{|}~';
+        $str='';
+        for ($i=0; $i<127; $i++) {
+             $str.=chr($i);
+        }
+        $this->assertEquals($expected, Zend_Ldap_Converter::ascToHex32($str));
+    }
+
+    public function testHex2asc() 
+    {
+        $expected='';
+        for ($i=0; $i<127; $i++) {
+             $expected.=chr($i);
+        }
+
+        $str='\00\01\02\03\04\05\06\07\08\09\0a\0b\0c\0d\0e\0f\10\11\12\13\14\15\16\17\18\19\1a\1b' . 
+            '\1c\1d\1e\1f !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefg' . 
+            'hijklmnopqrstuvwxyz{|}~';
+        $this->assertEquals($expected, Zend_Ldap_Converter::hex32ToAsc($str));
+    }
+}

+ 361 - 0
tests/Zend/Ldap/CopyRenameTest.php

@@ -0,0 +1,361 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Zend_Ldap_OnlineTestCase
+ */
+require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'OnlineTestCase.php';
+
+/**
+ * @see Zend_Ldap_Dn
+ */
+require_once 'Zend/Ldap/Dn.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+class Zend_Ldap_CopyRenameTest extends Zend_Ldap_OnlineTestCase
+{
+    /**
+     * @var string
+     */
+    private $_orgDn;
+    /**
+     * @var string
+     */
+    private $_newDn;
+    /**
+     * @var string
+     */
+    private $_orgSubTreeDn;
+    /**
+     * @var string
+     */
+    private $_newSubTreeDn;
+    /**
+     * @var string
+     */
+    private $_targetSubTreeDn;
+
+    /**
+     * @var array
+     */
+    private $_nodes;
+
+    protected function setUp()
+    {
+        parent::setUp();
+        $this->_prepareLdapServer();
+
+        $this->_orgDn=$this->_createDn('ou=OrgTest,');
+        $this->_newDn=$this->_createDn('ou=NewTest,');
+        $this->_orgSubTreeDn=$this->_createDn('ou=OrgSubtree,');
+        $this->_newSubTreeDn=$this->_createDn('ou=NewSubtree,');
+        $this->_targetSubTreeDn=$this->_createDn('ou=Target,');
+
+        $this->_nodes=array(
+            $this->_orgDn => array("objectClass" => "organizationalUnit", "ou" => "OrgTest"),
+            $this->_orgSubTreeDn =>  array("objectClass" => "organizationalUnit", "ou" => "OrgSubtree"),
+            'ou=Subtree1,' . $this->_orgSubTreeDn =>
+                array("objectClass" => "organizationalUnit", "ou" => "Subtree1"),
+            'ou=Subtree11,ou=Subtree1,' . $this->_orgSubTreeDn =>
+                array("objectClass" => "organizationalUnit", "ou" => "Subtree11"),
+            'ou=Subtree12,ou=Subtree1,' . $this->_orgSubTreeDn =>
+                array("objectClass" => "organizationalUnit", "ou" => "Subtree12"),
+            'ou=Subtree13,ou=Subtree1,' . $this->_orgSubTreeDn =>
+                array("objectClass" => "organizationalUnit", "ou" => "Subtree13"),
+            'ou=Subtree2,' . $this->_orgSubTreeDn =>
+                array("objectClass" => "organizationalUnit", "ou" => "Subtree2"),
+            'ou=Subtree3,' . $this->_orgSubTreeDn =>
+                array("objectClass" => "organizationalUnit", "ou" => "Subtree3"),
+            $this->_targetSubTreeDn => array("objectClass" => "organizationalUnit", "ou" => "Target")
+        );
+
+        $ldap=$this->_getLdap()->getResource();
+        foreach ($this->_nodes as $dn => $entry) {
+            ldap_add($ldap, $dn, $entry);
+        }
+    }
+
+    protected function tearDown()
+    {
+        if ($this->_getLdap()->exists($this->_newDn))
+            $this->_getLdap()->delete($this->_newDn, false);
+        if ($this->_getLdap()->exists($this->_orgDn))
+            $this->_getLdap()->delete($this->_orgDn, false);
+        if ($this->_getLdap()->exists($this->_orgSubTreeDn))
+            $this->_getLdap()->delete($this->_orgSubTreeDn, true);
+        if ($this->_getLdap()->exists($this->_newSubTreeDn))
+            $this->_getLdap()->delete($this->_newSubTreeDn, true);
+        if ($this->_getLdap()->exists($this->_targetSubTreeDn))
+            $this->_getLdap()->delete($this->_targetSubTreeDn, true);
+
+
+        $this->_cleanupLdapServer();
+        parent::tearDown();
+    }
+
+    public function testSimpleLeafRename()
+    {
+        $org=$this->_getLdap()->getEntry($this->_orgDn, array(), true);
+        $this->_getLdap()->rename($this->_orgDn, $this->_newDn, false);
+        $this->assertFalse($this->_getLdap()->exists($this->_orgDn));
+        $this->assertTrue($this->_getLdap()->exists($this->_newDn));
+        $new=$this->_getLdap()->getEntry($this->_newDn);
+        $this->assertEquals($org['objectclass'], $new['objectclass']);
+        $this->assertEquals(array('NewTest'), $new['ou']);
+    }
+
+    public function testSimpleLeafMoveAlias()
+    {
+        $this->_getLdap()->move($this->_orgDn, $this->_newDn, false);
+        $this->assertFalse($this->_getLdap()->exists($this->_orgDn));
+        $this->assertTrue($this->_getLdap()->exists($this->_newDn));
+    }
+
+    public function testSimpleLeafMoveToSubtree()
+    {
+        $this->_getLdap()->moveToSubtree($this->_orgDn, $this->_orgSubTreeDn, false);
+        $this->assertFalse($this->_getLdap()->exists($this->_orgDn));
+        $this->assertTrue($this->_getLdap()->exists('ou=OrgTest,' . $this->_orgSubTreeDn));
+    }
+
+    /**
+     * @expectedException Zend_Ldap_Exception
+     */
+    public function testRenameSourceNotExists()
+    {
+        $this->_getLdap()->rename($this->_createDn('ou=DoesNotExist,'), $this->_newDn, false);
+    }
+
+    /**
+     * @expectedException Zend_Ldap_Exception
+     */
+    public function testRenameTargetExists()
+    {
+        $this->_getLdap()->rename($this->_orgDn, $this->_createDn('ou=Test1,'), false);
+    }
+
+    /**
+     * @expectedException Zend_Ldap_Exception
+     */
+    public function testRenameTargetParentNotExists()
+    {
+        $this->_getLdap()->rename($this->_orgDn, $this->_createDn('ou=Test1,ou=ParentDoesNotExist,'), false);
+    }
+
+    /**
+     * @expectedException Zend_Ldap_Exception
+     */
+    public function testRenameEmulationSourceNotExists()
+    {
+        $this->_getLdap()->rename($this->_createDn('ou=DoesNotExist,'), $this->_newDn, false, true);
+    }
+
+    /**
+     * @expectedException Zend_Ldap_Exception
+     */
+    public function testRenameEmulationTargetExists()
+    {
+        $this->_getLdap()->rename($this->_orgDn, $this->_createDn('ou=Test1,'), false, true);
+    }
+
+    /**
+     * @expectedException Zend_Ldap_Exception
+     */
+    public function testRenameEmulationTargetParentNotExists()
+    {
+        $this->_getLdap()->rename($this->_orgDn, $this->_createDn('ou=Test1,ou=ParentDoesNotExist,'),
+            false, true);
+    }
+
+    public function testSimpleLeafRenameEmulation()
+    {
+        $this->_getLdap()->rename($this->_orgDn, $this->_newDn, false, true);
+        $this->assertFalse($this->_getLdap()->exists($this->_orgDn));
+        $this->assertTrue($this->_getLdap()->exists($this->_newDn));
+    }
+
+    public function testSimpleLeafCopyToSubtree()
+    {
+        $this->_getLdap()->copyToSubtree($this->_orgDn, $this->_orgSubTreeDn, false);
+        $this->assertTrue($this->_getLdap()->exists($this->_orgDn));
+        $this->assertTrue($this->_getLdap()->exists('ou=OrgTest,' . $this->_orgSubTreeDn));
+    }
+
+    public function testSimpleLeafCopy()
+    {
+        $this->_getLdap()->copy($this->_orgDn, $this->_newDn, false);
+        $this->assertTrue($this->_getLdap()->exists($this->_orgDn));
+        $this->assertTrue($this->_getLdap()->exists($this->_newDn));
+    }
+
+    public function testRecursiveRename()
+    {
+        $this->_getLdap()->rename($this->_orgSubTreeDn, $this->_newSubTreeDn, true);
+        $this->assertFalse($this->_getLdap()->exists($this->_orgSubTreeDn));
+        $this->assertTrue($this->_getLdap()->exists($this->_newSubTreeDn));
+        $this->assertEquals(3, $this->_getLdap()->countChildren($this->_newSubTreeDn));
+        $this->assertEquals(3, $this->_getLdap()->countChildren('ou=Subtree1,' . $this->_newSubTreeDn));
+    }
+
+    public function testRecursiveMoveToSubtree()
+    {
+        $this->_getLdap()->moveToSubtree($this->_orgSubTreeDn, $this->_targetSubTreeDn, true);
+        $this->assertFalse($this->_getLdap()->exists($this->_orgSubTreeDn));
+        $this->assertTrue($this->_getLdap()->exists('ou=OrgSubtree,' . $this->_targetSubTreeDn));
+        $this->assertEquals(3, $this->_getLdap()->countChildren('ou=OrgSubtree,' . $this->_targetSubTreeDn));
+        $this->assertEquals(3, $this->_getLdap()->countChildren('ou=Subtree1,ou=OrgSubtree,' . $this->_targetSubTreeDn));
+    }
+
+    public function testRecursiveCopyToSubtree()
+    {
+        $this->_getLdap()->copyToSubtree($this->_orgSubTreeDn, $this->_targetSubTreeDn, true);
+        $this->assertTrue($this->_getLdap()->exists($this->_orgSubTreeDn));
+        $this->assertTrue($this->_getLdap()->exists('ou=OrgSubtree,' . $this->_targetSubTreeDn));
+        $this->assertEquals(3, $this->_getLdap()->countChildren($this->_orgSubTreeDn));
+        $this->assertEquals(3, $this->_getLdap()->countChildren('ou=Subtree1,' . $this->_orgSubTreeDn));
+        $this->assertEquals(3, $this->_getLdap()->countChildren('ou=OrgSubtree,' . $this->_targetSubTreeDn));
+        $this->assertEquals(3, $this->_getLdap()->countChildren('ou=Subtree1,ou=OrgSubtree,' . $this->_targetSubTreeDn));
+    }
+
+    public function testRecursiveCopy()
+    {
+        $this->_getLdap()->copy($this->_orgSubTreeDn, $this->_newSubTreeDn, true);
+        $this->assertTrue($this->_getLdap()->exists($this->_orgSubTreeDn));
+        $this->assertTrue($this->_getLdap()->exists($this->_newSubTreeDn));
+        $this->assertEquals(3, $this->_getLdap()->countChildren($this->_orgSubTreeDn));
+        $this->assertEquals(3, $this->_getLdap()->countChildren('ou=Subtree1,' . $this->_orgSubTreeDn));
+        $this->assertEquals(3, $this->_getLdap()->countChildren($this->_newSubTreeDn));
+        $this->assertEquals(3, $this->_getLdap()->countChildren('ou=Subtree1,' . $this->_newSubTreeDn));
+    }
+
+    public function testSimpleLeafRenameWithDnObjects()
+    {
+        $orgDn=Zend_Ldap_Dn::fromString($this->_orgDn);
+        $newDn=Zend_Ldap_Dn::fromString($this->_newDn);
+
+        $this->_getLdap()->rename($orgDn, $newDn, false);
+        $this->assertFalse($this->_getLdap()->exists($orgDn));
+        $this->assertTrue($this->_getLdap()->exists($newDn));
+
+        $this->_getLdap()->move($newDn, $orgDn, false);
+        $this->assertTrue($this->_getLdap()->exists($orgDn));
+        $this->assertFalse($this->_getLdap()->exists($newDn));
+    }
+
+    public function testSimpleLeafMoveToSubtreeWithDnObjects()
+    {
+        $orgDn=Zend_Ldap_Dn::fromString($this->_orgDn);
+        $orgSubTreeDn=Zend_Ldap_Dn::fromString($this->_orgSubTreeDn);
+
+        $this->_getLdap()->moveToSubtree($orgDn, $orgSubTreeDn, false);
+        $this->assertFalse($this->_getLdap()->exists($orgDn));
+        $this->assertTrue($this->_getLdap()->exists('ou=OrgTest,' . $orgSubTreeDn->toString()));
+    }
+
+    public function testSimpleLeafRenameEmulationWithDnObjects()
+    {
+        $orgDn=Zend_Ldap_Dn::fromString($this->_orgDn);
+        $newDn=Zend_Ldap_Dn::fromString($this->_newDn);
+
+        $this->_getLdap()->rename($orgDn, $newDn, false, true);
+        $this->assertFalse($this->_getLdap()->exists($orgDn));
+        $this->assertTrue($this->_getLdap()->exists($newDn));
+    }
+
+    public function testSimpleLeafCopyToSubtreeWithDnObjects()
+    {
+        $orgDn=Zend_Ldap_Dn::fromString($this->_orgDn);
+        $orgSubTreeDn=Zend_Ldap_Dn::fromString($this->_orgSubTreeDn);
+
+        $this->_getLdap()->copyToSubtree($orgDn, $orgSubTreeDn, false);
+        $this->assertTrue($this->_getLdap()->exists($orgDn));
+        $this->assertTrue($this->_getLdap()->exists('ou=OrgTest,' . $orgSubTreeDn->toString()));
+    }
+
+    public function testSimpleLeafCopyWithDnObjects()
+    {
+        $orgDn=Zend_Ldap_Dn::fromString($this->_orgDn);
+        $newDn=Zend_Ldap_Dn::fromString($this->_newDn);
+
+        $this->_getLdap()->copy($orgDn, $newDn, false);
+        $this->assertTrue($this->_getLdap()->exists($orgDn));
+        $this->assertTrue($this->_getLdap()->exists($newDn));
+    }
+
+    public function testRecursiveRenameWithDnObjects()
+    {
+        $orgSubTreeDn=Zend_Ldap_Dn::fromString($this->_orgSubTreeDn);
+        $newSubTreeDn=Zend_Ldap_Dn::fromString($this->_newSubTreeDn);
+
+        $this->_getLdap()->rename($orgSubTreeDn, $newSubTreeDn, true);
+        $this->assertFalse($this->_getLdap()->exists($orgSubTreeDn));
+        $this->assertTrue($this->_getLdap()->exists($newSubTreeDn));
+        $this->assertEquals(3, $this->_getLdap()->countChildren($newSubTreeDn));
+        $this->assertEquals(3, $this->_getLdap()->countChildren('ou=Subtree1,' . $newSubTreeDn->toString()));
+    }
+
+    public function testRecursiveMoveToSubtreeWithDnObjects()
+    {
+        $orgSubTreeDn=Zend_Ldap_Dn::fromString($this->_orgSubTreeDn);
+        $targetSubTreeDn=Zend_Ldap_Dn::fromString($this->_targetSubTreeDn);
+
+        $this->_getLdap()->moveToSubtree($orgSubTreeDn, $targetSubTreeDn, true);
+        $this->assertFalse($this->_getLdap()->exists($orgSubTreeDn));
+        $this->assertTrue($this->_getLdap()->exists('ou=OrgSubtree,' . $targetSubTreeDn->toString()));
+        $this->assertEquals(3, $this->_getLdap()->countChildren('ou=OrgSubtree,' . $targetSubTreeDn->toString()));
+        $this->assertEquals(3, $this->_getLdap()->countChildren('ou=Subtree1,ou=OrgSubtree,' . $targetSubTreeDn->toString()));
+    }
+
+    public function testRecursiveCopyToSubtreeWithDnObjects()
+    {
+        $orgSubTreeDn=Zend_Ldap_Dn::fromString($this->_orgSubTreeDn);
+        $targetSubTreeDn=Zend_Ldap_Dn::fromString($this->_targetSubTreeDn);
+
+        $this->_getLdap()->copyToSubtree($orgSubTreeDn, $targetSubTreeDn, true);
+        $this->assertTrue($this->_getLdap()->exists($orgSubTreeDn));
+        $this->assertTrue($this->_getLdap()->exists('ou=OrgSubtree,' . $targetSubTreeDn->toString()));
+        $this->assertEquals(3, $this->_getLdap()->countChildren($orgSubTreeDn));
+        $this->assertEquals(3, $this->_getLdap()->countChildren('ou=Subtree1,' . $orgSubTreeDn->toString()));
+        $this->assertEquals(3, $this->_getLdap()->countChildren('ou=OrgSubtree,' . $targetSubTreeDn->toString()));
+        $this->assertEquals(3, $this->_getLdap()->countChildren('ou=Subtree1,ou=OrgSubtree,' . $targetSubTreeDn->toString()));
+    }
+
+    public function testRecursiveCopyWithDnObjects()
+    {
+        $orgSubTreeDn=Zend_Ldap_Dn::fromString($this->_orgSubTreeDn);
+        $newSubTreeDn=Zend_Ldap_Dn::fromString($this->_newSubTreeDn);
+
+        $this->_getLdap()->copy($orgSubTreeDn, $newSubTreeDn, true);
+        $this->assertTrue($this->_getLdap()->exists($orgSubTreeDn));
+        $this->assertTrue($this->_getLdap()->exists($newSubTreeDn));
+        $this->assertEquals(3, $this->_getLdap()->countChildren($orgSubTreeDn));
+        $this->assertEquals(3, $this->_getLdap()->countChildren('ou=Subtree1,' . $orgSubTreeDn->toString()));
+        $this->assertEquals(3, $this->_getLdap()->countChildren($newSubTreeDn));
+        $this->assertEquals(3, $this->_getLdap()->countChildren('ou=Subtree1,' . $newSubTreeDn->toString()));
+    }
+}

+ 356 - 0
tests/Zend/Ldap/CrudTest.php

@@ -0,0 +1,356 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Zend_Ldap_OnlineTestCase
+ */
+require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'OnlineTestCase.php';
+
+/**
+ * @see Zend_Ldap_Dn
+ */
+require_once 'Zend/Ldap/Dn.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+class Zend_Ldap_CrudTest extends Zend_Ldap_OnlineTestCase
+{
+    public function testAddAndDelete()
+    {
+        $dn=$this->_createDn('ou=TestCreated,');
+        $data=array(
+            'ou' => 'TestCreated',
+            'objectClass' => 'organizationalUnit'
+        );
+        try {
+            $this->_getLdap()->add($dn, $data);
+            $this->assertEquals(1, $this->_getLdap()->count('ou=TestCreated'));
+            $this->_getLdap()->delete($dn);
+            $this->assertEquals(0, $this->_getLdap()->count('ou=TestCreated'));
+        } catch (Zend_Ldap_Exception $e) {
+            if ($this->_getLdap()->exists($dn)) {
+                $this->_getLdap()->delete($dn);
+            }
+            $this->fail($e->getMessage());
+        }
+    }
+
+    public function testUpdate()
+    {
+        $dn=$this->_createDn('ou=TestCreated,');
+        $data=array(
+            'ou' => 'TestCreated',
+            'l' => 'mylocation1',
+            'objectClass' => 'organizationalUnit'
+        );
+        try {
+            $this->_getLdap()->add($dn, $data);
+            $entry=$this->_getLdap()->getEntry($dn);
+            $this->assertEquals('mylocation1', $entry['l'][0]);
+            $entry['l']='mylocation2';
+            $this->_getLdap()->update($dn, $entry);
+            $entry=$this->_getLdap()->getEntry($dn);
+            $this->_getLdap()->delete($dn);
+            $this->assertEquals('mylocation2', $entry['l'][0]);
+        } catch (Zend_Ldap_Exception $e) {
+            if ($this->_getLdap()->exists($dn)) {
+                $this->_getLdap()->delete($dn);
+            }
+            $this->fail($e->getMessage());
+        }
+    }
+
+    /**
+     * @expectedException Zend_Ldap_Exception
+     */
+    public function testIllegalAdd()
+    {
+        $dn=$this->_createDn('ou=TestCreated,ou=Node2,');
+        $data=array(
+            'ou' => 'TestCreated',
+            'objectClass' => 'organizationalUnit'
+        );
+        $this->_getLdap()->add($dn, $data);
+        $this->_getLdap()->delete($dn);
+    }
+
+    public function testIllegalUpdate()
+    {
+        $dn=$this->_createDn('ou=TestCreated,');
+        $data=array(
+            'ou' => 'TestCreated',
+            'objectclass' => 'organizationalUnit'
+        );
+        try {
+            $this->_getLdap()->add($dn, $data);
+            $entry=$this->_getLdap()->getEntry($dn);
+            $entry['objectclass'][]='inetOrgPerson';
+
+            $exThrown=false;
+            try {
+                $this->_getLdap()->update($dn, $entry);
+            }
+            catch (Zend_Ldap_Exception $e) {
+               $exThrown=true;
+            }
+            $this->_getLdap()->delete($dn);
+            if (!$exThrown) $this->fail('no exception thrown while illegaly updating entry');
+        }
+        catch (Zend_Ldap_Exception $e) {
+            $this->fail($e->getMessage());
+        }
+    }
+
+    /**
+     * @expectedException Zend_Ldap_Exception
+     */
+    public function testIllegalDelete()
+    {
+        $dn=$this->_createDn('ou=TestCreated,');
+        $this->_getLdap()->delete($dn);
+    }
+
+    public function testDeleteRecursively()
+    {
+        $topDn=$this->_createDn('ou=RecursiveTest,');
+        $dn=$topDn;
+        $data=array('ou' => 'RecursiveTest', 'objectclass' => 'organizationalUnit'
+        );
+        $this->_getLdap()->add($dn, $data);
+        for ($level=1; $level<=5; $level++) {
+            $name='Level' . $level;
+            $dn='ou=' . $name . ',' . $dn;
+            $data=array('ou' => $name, 'objectclass' => 'organizationalUnit');
+            $this->_getLdap()->add($dn, $data);
+            for ($item=1; $item<=5; $item++) {
+                $uid='Item' . $item;
+                $idn='ou=' . $uid . ',' . $dn;
+                $idata=array('ou' => $uid, 'objectclass' => 'organizationalUnit');
+                $this->_getLdap()->add($idn, $idata);
+            }
+        }
+
+        $exCaught=false;
+        try {
+            $this->_getLdap()->delete($topDn, false);
+        } catch (Zend_Ldap_Exception $e) {
+            $exCaught=true;
+        }
+        $this->assertTrue($exCaught,
+            'Execption not raised when deleting item with children without specifiying recursive delete');
+        $this->_getLdap()->delete($topDn, true);
+        $this->assertFalse($this->_getLdap()->exists($topDn));
+    }
+
+    public function testSave()
+    {
+        $dn=$this->_createDn('ou=TestCreated,');
+        $data=array('ou' => 'TestCreated', 'objectclass' => 'organizationalUnit');
+        try {
+            $this->_getLdap()->save($dn, $data);
+            $this->assertTrue($this->_getLdap()->exists($dn));
+            $data['l']='mylocation1';
+            $this->_getLdap()->save($dn, $data);
+            $this->assertTrue($this->_getLdap()->exists($dn));
+            $entry=$this->_getLdap()->getEntry($dn);
+            $this->_getLdap()->delete($dn);
+            $this->assertEquals('mylocation1', $entry['l'][0]);
+        } catch (Zend_Ldap_Exception $e) {
+            if ($this->_getLdap()->exists($dn)) {
+                $this->_getLdap()->delete($dn);
+            }
+            $this->fail($e->getMessage());
+        }
+
+    }
+
+    public function testPrepareLdapEntryArray()
+    {
+        $data=array(
+            'a1' => 'TestCreated',
+            'a2' => 'account',
+            'a3' => null,
+            'a4' => '',
+            'a5' => array('TestCreated'),
+            'a6' => array('account'),
+            'a7' => array(null),
+            'a8' => array(''),
+            'a9' => array('', null, 'account', '', null, 'TestCreated', '', null));
+        Zend_Ldap::prepareLdapEntryArray($data);
+        $expected=array(
+            'a1' => array('TestCreated'),
+            'a2' => array('account'),
+            'a3' => array(),
+            'a4' => array(),
+            'a5' => array('TestCreated'),
+            'a6' => array('account'),
+            'a7' => array(),
+            'a8' => array(),
+            'a9' => array('account', 'TestCreated'));
+        $this->assertEquals($expected, $data);
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testPrepareLdapEntryArrayArrayData()
+    {
+        $data=array(
+            'a1' => array(array('account')));
+        Zend_Ldap::prepareLdapEntryArray($data);
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     */
+    public function testPrepareLdapEntryArrayObjectData()
+    {
+        $class=new stdClass();
+        $class->a='b';
+        $data=array(
+            'a1' => array($class));
+        Zend_Ldap::prepareLdapEntryArray($data);
+    }
+
+    public function testAddWithDnObject()
+    {
+        $dn=Zend_Ldap_Dn::fromString($this->_createDn('ou=TestCreated,'));
+        $data=array(
+            'ou' => 'TestCreated',
+            'objectclass' => 'organizationalUnit'
+        );
+        try {
+            $this->_getLdap()->add($dn, $data);
+            $this->assertEquals(1, $this->_getLdap()->count('ou=TestCreated'));
+            $this->_getLdap()->delete($dn);
+        }
+        catch (Zend_Ldap_Exception $e) {
+            $this->fail($e->getMessage());
+        }
+    }
+
+    public function testUpdateWithDnObject()
+    {
+        $dn=Zend_Ldap_Dn::fromString($this->_createDn('ou=TestCreated,'));
+        $data=array(
+            'ou' => 'TestCreated',
+            'l' => 'mylocation1',
+            'objectclass' => 'organizationalUnit'
+        );
+        try {
+            $this->_getLdap()->add($dn, $data);
+            $entry=$this->_getLdap()->getEntry($dn);
+            $this->assertEquals('mylocation1', $entry['l'][0]);
+            $entry['l']='mylocation2';
+            $this->_getLdap()->update($dn, $entry);
+            $entry=$this->_getLdap()->getEntry($dn);
+            $this->_getLdap()->delete($dn);
+            $this->assertEquals('mylocation2', $entry['l'][0]);
+        }
+        catch (Zend_Ldap_Exception $e) {
+            $this->fail($e->getMessage());
+        }
+    }
+
+    public function testSaveWithDnObject()
+    {
+        $dn=Zend_Ldap_Dn::fromString($this->_createDn('ou=TestCreated,'));
+        $data=array('ou' => 'TestCreated', 'objectclass' => 'organizationalUnit');
+        try {
+            $this->_getLdap()->save($dn, $data);
+            $this->assertTrue($this->_getLdap()->exists($dn));
+            $data['l']='mylocation1';
+            $this->_getLdap()->save($dn, $data);
+            $this->assertTrue($this->_getLdap()->exists($dn));
+            $entry=$this->_getLdap()->getEntry($dn);
+            $this->_getLdap()->delete($dn);
+            $this->assertEquals('mylocation1', $entry['l'][0]);
+        } catch (Zend_Ldap_Exception $e) {
+            if ($this->_getLdap()->exists($dn)) {
+                $this->_getLdap()->delete($dn);
+            }
+            $this->fail($e->getMessage());
+        }
+    }
+
+    public function testAddObjectClass()
+    {
+        $dn=$this->_createDn('ou=TestCreated,');
+        $data=array(
+            'ou' => 'TestCreated',
+            'l' => 'mylocation1',
+            'objectClass' => 'organizationalUnit'
+        );
+        try {
+            $this->_getLdap()->add($dn, $data);
+            $entry=$this->_getLdap()->getEntry($dn);
+            $entry['objectclass'][]='domainRelatedObject';
+            $entry['associatedDomain'][]='domain';
+            $this->_getLdap()->update($dn, $entry);
+            $entry=$this->_getLdap()->getEntry($dn);
+            $this->_getLdap()->delete($dn);
+
+            $this->assertEquals('domain', $entry['associateddomain'][0]);
+            $this->assertContains('organizationalUnit', $entry['objectclass']);
+            $this->assertContains('domainRelatedObject', $entry['objectclass']);
+        } catch (Zend_Ldap_Exception $e) {
+            if ($this->_getLdap()->exists($dn)) {
+                $this->_getLdap()->delete($dn);
+            }
+            $this->fail($e->getMessage());
+        }
+    }
+
+    public function testRemoveObjectClass()
+    {
+        $dn=$this->_createDn('ou=TestCreated,');
+        $data=array(
+            'associatedDomain' => 'domain',
+            'ou' => 'TestCreated',
+            'l' => 'mylocation1',
+            'objectClass' => array('organizationalUnit', 'domainRelatedObject')
+        );
+        try {
+            $this->_getLdap()->add($dn, $data);
+            $entry=$this->_getLdap()->getEntry($dn);
+            $entry['objectclass']='organizationalUnit';
+            $entry['associatedDomain']=null;
+            $this->_getLdap()->update($dn, $entry);
+            $entry=$this->_getLdap()->getEntry($dn);
+            $this->_getLdap()->delete($dn);
+
+            $this->assertArrayNotHasKey('associateddomain', $entry);
+            $this->assertContains('organizationalUnit', $entry['objectclass']);
+            $this->assertNotContains('domainRelatedObject', $entry['objectclass']);
+        } catch (Zend_Ldap_Exception $e) {
+            if ($this->_getLdap()->exists($dn)) {
+                $this->_getLdap()->delete($dn);
+            }
+            $this->fail($e->getMessage());
+        }
+    }
+}

+ 87 - 0
tests/Zend/Ldap/Dn/AllTests.php

@@ -0,0 +1,87 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * Test helper
+ */
+require_once dirname(dirname(dirname(dirname(__FILE__)))) . DIRECTORY_SEPARATOR . 'TestHelper.php';
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_Ldap_Dn_AllTests::main');
+}
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Dn_AllTests
+{
+    public static function main()
+    {
+        PHPUnit_TextUI_TestRunner::run(self::suite());
+    }
+
+    public static function suite()
+    {
+        $suite = new PHPUnit_Framework_TestSuite('Zend Framework - Zend_Ldap_Dn');
+
+        /**
+         * @see Zend_Ldap_Dn_EscapingTest
+         */
+        require_once 'Zend/Ldap/Dn/EscapingTest.php';
+        $suite->addTestSuite('Zend_Ldap_Dn_EscapingTest');
+        /**
+         * @see Zend_Ldap_Dn_ExplodingTest
+         */
+        require_once 'Zend/Ldap/Dn/ExplodingTest.php';
+        $suite->addTestSuite('Zend_Ldap_Dn_ExplodingTest');
+        /**
+         * @see Zend_Ldap_Dn_ImplodingTest
+         */
+        require_once 'Zend/Ldap/Dn/ImplodingTest.php';
+        $suite->addTestSuite('Zend_Ldap_Dn_ImplodingTest');
+        /**
+         * @see Zend_Ldap_Dn_CreationTest
+         */
+        require_once 'Zend/Ldap/Dn/CreationTest.php';
+        $suite->addTestSuite('Zend_Ldap_Dn_CreationTest');
+        /**
+         * @see Zend_Ldap_Dn_ModificationTest
+         */
+        require_once 'Zend/Ldap/Dn/ModificationTest.php';
+        $suite->addTestSuite('Zend_Ldap_Dn_ModificationTest');
+        /**
+         * @see Zend_Ldap_Dn_MiscTest
+         */
+        require_once 'Zend/Ldap/Dn/MiscTest.php';
+        $suite->addTestSuite('Zend_Ldap_Dn_MiscTest');
+
+        return $suite;
+    }
+}
+
+if (PHPUnit_MAIN_METHOD == 'Zend_Ldap_Dn_AllTests::main') {
+    Zend_Ldap_Dn_AllTests::main();
+}

+ 212 - 0
tests/Zend/Ldap/Dn/CreationTest.php

@@ -0,0 +1,212 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Test helper
+ */
+require_once dirname(dirname(dirname(dirname(__FILE__)))) . DIRECTORY_SEPARATOR . 'TestHelper.php';
+/**
+ * Zend_Ldap_Dn
+ */
+require_once 'Zend/Ldap/Dn.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+class Zend_Ldap_Dn_CreationTest extends PHPUnit_Framework_TestCase
+{
+    public function testDnCreation()
+    {
+        Zend_Ldap_Dn::setDefaultCaseFold(Zend_Ldap_Dn::ATTR_CASEFOLD_NONE);
+
+        $dnString1='CN=Baker\\, Alice,CN=Users+OU=Lab,DC=example,DC=com';
+        $dnArray1=array(
+            array('CN' => 'Baker, Alice'),
+            array('CN' => 'Users', 'OU' => 'Lab'),
+            array('DC' => 'example'),
+            array('DC' => 'com'));
+
+        $dnString2='cn=Baker\\, Alice,cn=Users+ou=Lab,dc=example,dc=com';
+        $dnArray2=array(
+            array('cn' => 'Baker, Alice'),
+            array('cn' => 'Users', 'ou' => 'Lab'),
+            array('dc' => 'example'),
+            array('dc' => 'com'));
+
+        $dnString3='Cn=Baker\\, Alice,Cn=Users+Ou=Lab,Dc=example,Dc=com';
+        $dnArray3=array(
+            array('Cn' => 'Baker, Alice'),
+            array('Cn' => 'Users', 'Ou' => 'Lab'),
+            array('Dc' => 'example'),
+            array('Dc' => 'com'));
+
+        $dn11=Zend_Ldap_Dn::fromString($dnString1);
+        $dn12=Zend_Ldap_Dn::fromArray($dnArray1);
+        $dn13=Zend_Ldap_Dn::factory($dnString1);
+        $dn14=Zend_Ldap_Dn::factory($dnArray1);
+
+        $this->assertEquals($dn11, $dn12);
+        $this->assertEquals($dn11, $dn13);
+        $this->assertEquals($dn11, $dn14);
+
+        $this->assertEquals($dnString1, $dn11->toString());
+        $this->assertEquals($dnString1, $dn11->toString(Zend_Ldap_Dn::ATTR_CASEFOLD_UPPER));
+        $this->assertEquals($dnString2, $dn11->toString(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER));
+        $this->assertEquals($dnArray1, $dn11->toArray());
+        $this->assertEquals($dnArray1, $dn11->toArray(Zend_Ldap_Dn::ATTR_CASEFOLD_UPPER));
+        $this->assertEquals($dnArray2, $dn11->toArray(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER));
+
+        $dn21=Zend_Ldap_Dn::fromString($dnString2);
+        $dn22=Zend_Ldap_Dn::fromArray($dnArray2);
+        $dn23=Zend_Ldap_Dn::factory($dnString2);
+        $dn24=Zend_Ldap_Dn::factory($dnArray2);
+
+        $this->assertEquals($dn21, $dn22);
+        $this->assertEquals($dn21, $dn23);
+        $this->assertEquals($dn21, $dn24);
+
+        $this->assertEquals($dnString2, $dn21->toString());
+        $this->assertEquals($dnString1, $dn21->toString(Zend_Ldap_Dn::ATTR_CASEFOLD_UPPER));
+        $this->assertEquals($dnString2, $dn21->toString(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER));
+        $this->assertEquals($dnArray2, $dn21->toArray());
+        $this->assertEquals($dnArray1, $dn21->toArray(Zend_Ldap_Dn::ATTR_CASEFOLD_UPPER));
+        $this->assertEquals($dnArray2, $dn21->toArray(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER));
+        $this->assertEquals($dnArray2, $dn22->toArray());
+
+        $dn31=Zend_Ldap_Dn::fromString($dnString3);
+        $dn32=Zend_Ldap_Dn::fromArray($dnArray3);
+        $dn33=Zend_Ldap_Dn::factory($dnString3);
+        $dn34=Zend_Ldap_Dn::factory($dnArray3);
+
+        $this->assertEquals($dn31, $dn32);
+        $this->assertEquals($dn31, $dn33);
+        $this->assertEquals($dn31, $dn34);
+
+        $this->assertEquals($dnString3, $dn31->toString());
+        $this->assertEquals($dnString1, $dn31->toString(Zend_Ldap_Dn::ATTR_CASEFOLD_UPPER));
+        $this->assertEquals($dnString2, $dn31->toString(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER));
+        $this->assertEquals($dnArray3, $dn31->toArray());
+        $this->assertEquals($dnArray1, $dn31->toArray(Zend_Ldap_Dn::ATTR_CASEFOLD_UPPER));
+        $this->assertEquals($dnArray2, $dn31->toArray(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER));
+
+        try {
+            $dn=Zend_Ldap_Dn::factory(1);
+            $this->fail('Expected Zend_Ldap_Exception not thrown');
+        } catch (Zend_Ldap_Exception $e) {
+            $this->assertEquals('Invalid argument type for $dn', $e->getMessage());
+        }
+    }
+
+    public function testDnCreationWithDifferentCaseFoldings()
+    {
+        Zend_Ldap_Dn::setDefaultCaseFold(Zend_Ldap_Dn::ATTR_CASEFOLD_NONE);
+
+        $dnString1='Cn=Baker\\, Alice,Cn=Users+Ou=Lab,Dc=example,Dc=com';
+        $dnString2='CN=Baker\\, Alice,CN=Users+OU=Lab,DC=example,DC=com';
+        $dnString3='cn=Baker\\, Alice,cn=Users+ou=Lab,dc=example,dc=com';
+
+        $dn=Zend_Ldap_Dn::fromString($dnString1, null);
+        $this->assertEquals($dnString1, (string)$dn);
+        $dn->setCaseFold(Zend_Ldap_Dn::ATTR_CASEFOLD_UPPER);
+        $this->assertEquals($dnString2, (string)$dn);
+        $dn->setCaseFold(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER);
+        $this->assertEquals($dnString3, (string)$dn);
+
+        $dn=Zend_Ldap_Dn::fromString($dnString1, Zend_Ldap_Dn::ATTR_CASEFOLD_UPPER);
+        $this->assertEquals($dnString2, (string)$dn);
+        $dn->setCaseFold(null);
+        $this->assertEquals($dnString1, (string)$dn);
+        $dn->setCaseFold(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER);
+        $this->assertEquals($dnString3, (string)$dn);
+
+        $dn=Zend_Ldap_Dn::fromString($dnString1, Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER);
+        $this->assertEquals($dnString3, (string)$dn);
+        $dn->setCaseFold(Zend_Ldap_Dn::ATTR_CASEFOLD_UPPER);
+        $this->assertEquals($dnString2, (string)$dn);
+        $dn->setCaseFold(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER);
+        $this->assertEquals($dnString3, (string)$dn);
+        $dn->setCaseFold(Zend_Ldap_Dn::ATTR_CASEFOLD_UPPER);
+        $this->assertEquals($dnString2, (string)$dn);
+
+        Zend_Ldap_Dn::setDefaultCaseFold(Zend_Ldap_Dn::ATTR_CASEFOLD_UPPER);
+        $dn=Zend_Ldap_Dn::fromString($dnString1, null);
+        $this->assertEquals($dnString2, (string)$dn);
+
+        Zend_Ldap_Dn::setDefaultCaseFold(null);
+        $dn=Zend_Ldap_Dn::fromString($dnString1, null);
+        $this->assertEquals($dnString1, (string)$dn);
+
+        Zend_Ldap_Dn::setDefaultCaseFold(Zend_Ldap_Dn::ATTR_CASEFOLD_NONE);
+    }
+
+    public function testGetRdn()
+    {
+        Zend_Ldap_Dn::setDefaultCaseFold(Zend_Ldap_Dn::ATTR_CASEFOLD_NONE);
+
+        $dnString='cn=Baker\\, Alice,cn=Users,dc=example,dc=com';
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+
+        $this->assertEquals(array('cn' => 'Baker, Alice'), $dn->getRdn());
+        $this->assertEquals('cn=Baker\\, Alice', $dn->getRdnString());
+
+        $dnString = 'Cn=Users+Ou=Lab,dc=example,dc=com';
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+        $this->assertEquals(array('Cn' => 'Users', 'Ou' => 'Lab'), $dn->getRdn());
+        $this->assertEquals('Cn=Users+Ou=Lab', $dn->getRdnString());
+    }
+
+    public function testGetParentDn()
+    {
+        $dnString='cn=Baker\\, Alice,cn=Users,dc=example,dc=com';
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+
+        $this->assertEquals('cn=Users,dc=example,dc=com', $dn->getParentDn()->toString());
+        $this->assertEquals('cn=Users,dc=example,dc=com', $dn->getParentDn(1)->toString());
+        $this->assertEquals('dc=example,dc=com', $dn->getParentDn(2)->toString());
+        $this->assertEquals('dc=com', $dn->getParentDn(3)->toString());
+
+        try {
+            $dn->getParentDn(0)->toString();
+            $this->fail('Expected Zend_Ldap_Exception not thrown');
+        } catch (Zend_Ldap_Exception $e) {
+            $this->assertEquals('Cannot retrieve parent DN with given $levelUp', $e->getMessage());
+        }
+        try {
+            $dn->getParentDn(4)->toString();
+            $this->fail('Expected Zend_Ldap_Exception not thrown');
+        } catch (Zend_Ldap_Exception $e) {
+            $this->assertEquals('Cannot retrieve parent DN with given $levelUp', $e->getMessage());
+        }
+    }
+
+    public function testEmptyStringDn()
+    {
+        $dnString='';
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+
+        $this->assertEquals($dnString, $dn->toString());
+    }
+}

+ 60 - 0
tests/Zend/Ldap/Dn/EscapingTest.php

@@ -0,0 +1,60 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Test helper
+ */
+require_once dirname(dirname(dirname(dirname(__FILE__)))) . DIRECTORY_SEPARATOR . 'TestHelper.php';
+/**
+ * Zend_Ldap_Dn
+ */
+require_once 'Zend/Ldap/Dn.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+class Zend_Ldap_Dn_EscapingTest extends PHPUnit_Framework_TestCase
+{
+    public function testEscapeValues()
+    {
+        $dnval='  '.chr(22).' t,e+s"t,\\v<a>l;u#e=!    ';
+        $expected='\20\20\16 t\,e\+s\"t\,\\\\v\<a\>l\;u\#e\=!\20\20\20\20';
+        $this->assertEquals($expected, Zend_Ldap_Dn::escapeValue($dnval));
+        $this->assertEquals($expected, Zend_Ldap_Dn::escapeValue(array($dnval)));
+        $this->assertEquals(array($expected, $expected, $expected),
+            Zend_Ldap_Dn::escapeValue(array($dnval, $dnval, $dnval)));
+    }
+
+    public function testUnescapeValues()
+    {
+        $dnval='\\20\\20\\16\\20t\\,e\\+s \\"t\\,\\\\v\\<a\\>l\\;u\\#e\\=!\\20\\20\\20\\20';
+        $expected='  '.chr(22).' t,e+s "t,\\v<a>l;u#e=!    ';
+        $this->assertEquals($expected, Zend_Ldap_Dn::unescapeValue($dnval));
+        $this->assertEquals($expected, Zend_Ldap_Dn::unescapeValue(array($dnval)));
+        $this->assertEquals(array($expected, $expected, $expected),
+            Zend_Ldap_Dn::unescapeValue(array($dnval,$dnval,$dnval)));
+    }
+}

+ 265 - 0
tests/Zend/Ldap/Dn/ExplodingTest.php

@@ -0,0 +1,265 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Test helper
+ */
+require_once dirname(dirname(dirname(dirname(__FILE__)))) . DIRECTORY_SEPARATOR . 'TestHelper.php';
+/**
+ * Zend_Ldap_Dn
+ */
+require_once 'Zend/Ldap/Dn.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+class Zend_Ldap_Dn_ExplodingTest extends PHPUnit_Framework_TestCase
+{
+    public static function explodeDnOperationProvider()
+    {
+        $testData = array(
+            array('CN=Alice Baker,CN=Users,DC=example,DC=com', true),
+            array('CN=Baker\\, Alice,CN=Users,DC=example,DC=com', true),
+            array('OU=Sales,DC=local', true),
+            array('OU=Sales;DC=local', true),
+            array('OU=Sales ,DC=local', true),
+            array('OU=Sales, dC=local', true),
+            array('ou=Sales , DC=local', true),
+            array('OU=Sales ; dc=local', true),
+            array('DC=local', true),
+            array(' DC=local', true),
+            array('DC= local  ', true),
+            array('username', false),
+            array('username@example.com', false),
+            array('EXAMPLE\\username', false),
+            array('CN=,Alice Baker,CN=Users,DC=example,DC=com', false),
+            array('CN=Users,DC==example,DC=com', false),
+            array('O=ACME', true),
+            array('', false),
+            array('   ', false),
+            array('uid=rogasawara,ou=営業部,o=Airius', true),
+            array('cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com', true),
+            array('CN=Steve Kille,O=Isode Limited,C=GB', true),
+            array('OU=Sales+CN=J. Smith,O=Widget Inc.,C=US', true),
+            array('CN=L. Eagle,O=Sue\, Grabbit and Runn,C=GB', true),
+            array('CN=Before\0DAfter,O=Test,C=GB', true),
+            array('SN=Lu\C4\8Di\C4\87', true),
+            array('OU=Sales+,O=Widget Inc.,C=US', false),
+            array('+OU=Sales,O=Widget Inc.,C=US', false),
+            array('OU=Sa+les,O=Widget Inc.,C=US', false),
+        );
+        return $testData;
+    }
+
+    /**
+     * @dataProvider explodeDnOperationProvider
+     */
+    public function testExplodeDnOperation($input, $expected)
+    {
+        $ret = Zend_Ldap_Dn::checkDn($input);
+        $this->assertTrue($ret === $expected);
+    }
+
+    public function testExplodeDnCaseFold()
+    {
+        $dn='CN=Alice Baker,cn=Users,DC=example,dc=com';
+        $k=array();
+        $v=null;
+        $this->assertTrue(Zend_Ldap_Dn::checkDn($dn, $k, $v, Zend_Ldap_Dn::ATTR_CASEFOLD_NONE));
+        $this->assertEquals(array('CN', 'cn', 'DC', 'dc'), $k);
+
+        $this->assertTrue(Zend_Ldap_Dn::checkDn($dn, $k, $v, Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER));
+        $this->assertEquals(array('cn', 'cn', 'dc', 'dc'), $k);
+
+        $this->assertTrue(Zend_Ldap_Dn::checkDn($dn, $k, $v, Zend_Ldap_Dn::ATTR_CASEFOLD_UPPER));
+        $this->assertEquals(array('CN', 'CN', 'DC', 'DC'), $k);
+    }
+
+    public function testExplodeDn()
+    {
+        $dn='cn=name1,cn=name2,dc=example,dc=org';
+        $k=array();
+        $v=array();
+        $dnArray=Zend_Ldap_Dn::explodeDn($dn, $k, $v);
+        $expected=array(
+            array("cn" => "name1"),
+            array("cn" => "name2"),
+            array("dc" => "example"),
+            array("dc" => "org")
+        );
+        $ke=array('cn', 'cn', 'dc', 'dc');
+        $ve=array('name1', 'name2', 'example', 'org');
+        $this->assertEquals($expected, $dnArray);
+        $this->assertEquals($ke, $k);
+        $this->assertEquals($ve, $v);
+    }
+
+    public function testExplodeDnWithUtf8Characters()
+    {
+        $dn='uid=rogasawara,ou=営業部,o=Airius';
+        $k=array();
+        $v=array();
+        $dnArray=Zend_Ldap_Dn::explodeDn($dn, $k, $v);
+        $expected=array(
+            array("uid" => "rogasawara"),
+            array("ou" => "営業部"),
+            array("o" => "Airius"),
+        );
+        $ke=array('uid', 'ou', 'o');
+        $ve=array('rogasawara', '営業部', 'Airius');
+        $this->assertEquals($expected, $dnArray);
+        $this->assertEquals($ke, $k);
+        $this->assertEquals($ve, $v);
+    }
+
+    public function testExplodeDnWithSpaces()
+    {
+        $dn='cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com';
+        $k=array();
+        $v=array();
+        $dnArray=Zend_Ldap_Dn::explodeDn($dn, $k, $v);
+        $expected=array(
+            array("cn" => "Barbara Jensen"),
+            array("ou" => "Product Development"),
+            array("dc" => "airius"),
+            array("dc" => "com"),
+        );
+        $ke=array('cn', 'ou', 'dc', 'dc');
+        $ve=array('Barbara Jensen', 'Product Development', 'airius', 'com');
+        $this->assertEquals($expected, $dnArray);
+        $this->assertEquals($ke, $k);
+        $this->assertEquals($ve, $v);
+    }
+
+    public function testCoreExplodeDnWithMultiValuedRdn()
+    {
+        $dn='cn=name1+uid=user,cn=name2,dc=example,dc=org';
+        $k=array();
+        $v=array();
+        $this->assertTrue(Zend_Ldap_Dn::checkDn($dn, $k, $v));
+        $ke=array(array('cn', 'uid'), 'cn', 'dc', 'dc');
+        $ve=array(array('name1', 'user'), 'name2', 'example', 'org');
+        $this->assertEquals($ke, $k);
+        $this->assertEquals($ve, $v);
+
+        $dn='cn=name11+cn=name12,cn=name2,dc=example,dc=org';
+        $this->assertFalse(Zend_Ldap_Dn::checkDn($dn));
+
+        $dn='CN=name11+Cn=name12,cn=name2,dc=example,dc=org';
+        $this->assertFalse(Zend_Ldap_Dn::checkDn($dn));
+    }
+
+    public function testExplodeDnWithMultiValuedRdn()
+    {
+        $dn='cn=Surname\, Firstname+uid=userid,cn=name2,dc=example,dc=org';
+        $k=array();
+        $v=array();
+        $dnArray=Zend_Ldap_Dn::explodeDn($dn, $k, $v);
+        $ke=array(array('cn', 'uid'), 'cn', 'dc', 'dc');
+        $ve=array(array('Surname, Firstname', 'userid'), 'name2', 'example', 'org');
+        $this->assertEquals($ke, $k);
+        $this->assertEquals($ve, $v);
+        $expected=array(
+            array("cn" => "Surname, Firstname", "uid" => "userid"),
+            array("cn" => "name2"),
+            array("dc" => "example"),
+            array("dc" => "org")
+        );
+        $this->assertEquals($expected, $dnArray);
+    }
+
+    public function testExplodeDnWithMultiValuedRdn2()
+    {
+        $dn='cn=Surname\, Firstname+uid=userid+sn=Surname,cn=name2,dc=example,dc=org';
+        $k=array();
+        $v=array();
+        $dnArray=Zend_Ldap_Dn::explodeDn($dn, $k, $v);
+        $ke=array(array('cn', 'uid', 'sn'), 'cn', 'dc', 'dc');
+        $ve=array(array('Surname, Firstname', 'userid', 'Surname'), 'name2', 'example', 'org');
+        $this->assertEquals($ke, $k);
+        $this->assertEquals($ve, $v);
+        $expected=array(
+            array("cn" => "Surname, Firstname", "uid" => "userid", "sn" => "Surname"),
+            array("cn" => "name2"),
+            array("dc" => "example"),
+            array("dc" => "org")
+        );
+        $this->assertEquals($expected, $dnArray);
+    }
+
+    /**
+     * @expectedException Zend_Ldap_Exception
+     */
+    public function testCreateDnArrayIllegalDn()
+    {
+        $dn='name1,cn=name2,dc=example,dc=org';
+        $dnArray=Zend_Ldap_Dn::explodeDn($dn);
+    }
+
+    public static function rfc2253DnProvider()
+    {
+        $testData = array(
+            array('CN=Steve Kille,O=Isode Limited,C=GB',
+                array(
+                    array('CN' => 'Steve Kille'),
+                    array('O'  => 'Isode Limited'),
+                    array('C'  => 'GB')
+                )),
+            array('OU=Sales+CN=J. Smith,O=Widget Inc.,C=US',
+                array(
+                    array('OU' => 'Sales', 'CN' => 'J. Smith'),
+                    array('O'  => 'Widget Inc.'),
+                    array('C'  => 'US')
+                )),
+            array('CN=L. Eagle,O=Sue\, Grabbit and Runn,C=GB',
+                array(
+                    array('CN' => 'L. Eagle'),
+                    array('O'  => 'Sue, Grabbit and Runn'),
+                    array('C'  => 'GB')
+                )),
+            array('CN=Before\0DAfter,O=Test,C=GB',
+                array(
+                    array('CN' => "Before\rAfter"),
+                    array('O'  => 'Test'),
+                    array('C'  => 'GB')
+                )),
+            array('SN=Lu\C4\8Di\C4\87',
+                array(
+                    array('SN' => 'Lučić')
+                ))
+        );
+        return $testData;
+    }
+
+    /**
+     * @dataProvider rfc2253DnProvider
+     */
+    public function testExplodeDnsProvidedByRFC2253($input, $expected)
+    {
+        $dnArray=Zend_Ldap_Dn::explodeDn($input);
+        $this->assertEquals($expected, $dnArray);
+    }
+}

+ 139 - 0
tests/Zend/Ldap/Dn/ImplodingTest.php

@@ -0,0 +1,139 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Test helper
+ */
+require_once dirname(dirname(dirname(dirname(__FILE__)))) . DIRECTORY_SEPARATOR . 'TestHelper.php';
+/**
+ * Zend_Ldap_Dn
+ */
+require_once 'Zend/Ldap/Dn.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+class Zend_Ldap_Dn_ImplodingTest extends PHPUnit_Framework_TestCase
+{
+    public function testDnWithMultiValuedRdnRoundTrip()
+    {
+        $dn1='cn=Surname\, Firstname+uid=userid,cn=name2,dc=example,dc=org';
+        $dnArray=Zend_Ldap_Dn::explodeDn($dn1);
+        $dn2=Zend_Ldap_Dn::implodeDn($dnArray);
+        $this->assertEquals($dn1, $dn2);
+    }
+
+    public function testImplodeDn()
+    {
+        $expected='cn=name1,cn=name2,dc=example,dc=org';
+        $dnArray=array(
+            array("cn" => "name1"),
+            array("cn" => "name2"),
+            array("dc" => "example"),
+            array("dc" => "org")
+        );
+        $dn=Zend_Ldap_Dn::implodeDn($dnArray);
+        $this->assertEquals($expected, $dn);
+
+        $dn=Zend_Ldap_Dn::implodeDn($dnArray, Zend_Ldap_Dn::ATTR_CASEFOLD_UPPER, ';');
+        $this->assertEquals('CN=name1;CN=name2;DC=example;DC=org', $dn);
+    }
+
+    public function testImplodeDnWithUtf8Characters()
+    {
+        $expected='uid=rogasawara,ou=営業部,o=Airius';
+        $dnArray=array(
+            array("uid" => "rogasawara"),
+            array("ou" => "営業部"),
+            array("o" => "Airius"),
+        );
+        $dn=Zend_Ldap_Dn::implodeDn($dnArray);
+        $this->assertEquals($expected, $dn);
+    }
+
+    public function testImplodeRdn()
+    {
+        $a=array('cn' => 'value');
+        $expected='cn=value';
+        $this->assertEquals($expected, Zend_Ldap_Dn::implodeRdn($a));
+    }
+
+    public function testImplodeRdnMultiValuedRdn()
+    {
+        $a=array('cn' => 'value', 'uid' => 'testUser');
+        $expected='cn=value+uid=testUser';
+        $this->assertEquals($expected, Zend_Ldap_Dn::implodeRdn($a));
+    }
+
+    public function testImplodeRdnMultiValuedRdn2()
+    {
+        $a=array('cn' => 'value', 'uid' => 'testUser', 'ou' => 'myDep');
+        $expected='cn=value+ou=myDep+uid=testUser';
+        $this->assertEquals($expected, Zend_Ldap_Dn::implodeRdn($a));
+    }
+
+    public function testImplodeRdnCaseFold()
+    {
+        $a=array('cn' => 'value');
+        $expected='CN=value';
+        $this->assertEquals($expected,
+            Zend_Ldap_Dn::implodeRdn($a, Zend_Ldap_Dn::ATTR_CASEFOLD_UPPER));
+        $a=array('CN' => 'value');
+        $expected='cn=value';
+        $this->assertEquals($expected,
+            Zend_Ldap_Dn::implodeRdn($a, Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER));
+    }
+
+    public function testImplodeRdnMultiValuedRdnCaseFold()
+    {
+        $a=array('cn' => 'value', 'uid' => 'testUser', 'ou' => 'myDep');
+        $expected='CN=value+OU=myDep+UID=testUser';
+        $this->assertEquals($expected,
+            Zend_Ldap_Dn::implodeRdn($a, Zend_Ldap_Dn::ATTR_CASEFOLD_UPPER));
+        $a=array('CN' => 'value', 'uID' => 'testUser', 'ou' => 'myDep');
+        $expected='cn=value+ou=myDep+uid=testUser';
+        $this->assertEquals($expected,
+            Zend_Ldap_Dn::implodeRdn($a, Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER));
+    }
+
+    /**
+     * @expectedException Zend_Ldap_Exception
+     */
+    public function testImplodeRdnInvalidOne()
+    {
+        $a=array('cn');
+        Zend_Ldap_Dn::implodeRdn($a);
+    }
+
+    /**
+     * @expectedException Zend_Ldap_Exception
+     */
+    public function testImplodeRdnInvalidThree()
+    {
+        $a=array('cn' => 'value', 'ou');
+        Zend_Ldap_Dn::implodeRdn($a);
+    }
+}

+ 89 - 0
tests/Zend/Ldap/Dn/MiscTest.php

@@ -0,0 +1,89 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Test helper
+ */
+require_once dirname(dirname(dirname(dirname(__FILE__)))) . DIRECTORY_SEPARATOR . 'TestHelper.php';
+/**
+ * Zend_Ldap_Dn
+ */
+require_once 'Zend/Ldap/Dn.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+class Zend_Ldap_Dn_MiscTest extends PHPUnit_Framework_TestCase
+{
+    public function testIsChildOfIllegalDn1()
+    {
+        $dn1='name1,cn=name2,dc=example,dc=org';
+        $dn2='dc=example,dc=org';
+        $this->assertFalse(Zend_Ldap_Dn::isChildOf($dn1, $dn2));
+    }
+
+    public function testIsChildOfIllegalDn2()
+    {
+        $dn1='cn=name1,cn=name2,dc=example,dc=org';
+        $dn2='example,dc=org';
+        $this->assertFalse(Zend_Ldap_Dn::isChildOf($dn1, $dn2));
+    }
+
+    public function testIsChildOfIllegalBothDn()
+    {
+        $dn1='name1,cn=name2,dc=example,dc=org';
+        $dn2='example,dc=org';
+        $this->assertFalse(Zend_Ldap_Dn::isChildOf($dn1, $dn2));
+    }
+
+    public function testIsChildOf()
+    {
+        $dn1='cb=name1,cn=name2,dc=example,dc=org';
+        $dn2='dc=example,dc=org';
+        $this->assertTrue(Zend_Ldap_Dn::isChildOf($dn1, $dn2));
+    }
+
+    public function testIsChildOfWithDnObjects()
+    {
+        $dn1=Zend_Ldap_Dn::fromString('cb=name1,cn=name2,dc=example,dc=org');
+        $dn2=Zend_Ldap_Dn::fromString('dc=example,dc=org');
+        $this->assertTrue(Zend_Ldap_Dn::isChildOf($dn1, $dn2));
+    }
+
+    public function testIsChildOfOtherSubtree()
+    {
+        $dn1='cb=name1,cn=name2,dc=example,dc=org';
+        $dn2='dc=example,dc=de';
+        $this->assertFalse(Zend_Ldap_Dn::isChildOf($dn1, $dn2));
+    }
+
+    public function testIsChildOfParentDnLonger()
+    {
+        $dn1='dc=example,dc=de';
+        $dn2='cb=name1,cn=name2,dc=example,dc=org';
+        $this->assertFalse(Zend_Ldap_Dn::isChildOf($dn1, $dn2));
+    }
+}

+ 296 - 0
tests/Zend/Ldap/Dn/ModificationTest.php

@@ -0,0 +1,296 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Test helper
+ */
+require_once dirname(dirname(dirname(dirname(__FILE__)))) . DIRECTORY_SEPARATOR . 'TestHelper.php';
+/**
+ * Zend_Ldap_Dn
+ */
+require_once 'Zend/Ldap/Dn.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+class Zend_Ldap_Dn_ModificationTest extends PHPUnit_Framework_TestCase
+{
+    public function testDnManipulationGet()
+    {
+        $dnString='cn=Baker\\, Alice,cn=Users+ou=Lab,dc=example,dc=com';
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+
+        $this->assertEquals(array('cn' => 'Baker, Alice'), $dn->get(0));
+        $this->assertEquals(array('cn' => 'Users', 'ou' => 'Lab'), $dn->get(1));
+        $this->assertEquals(array('dc' => 'example'), $dn->get(2));
+        $this->assertEquals(array('dc' => 'com'), $dn->get(3));
+        try {
+            $this->assertEquals(array('dc' => 'com'), $dn->get('string'));
+            $this->fail('Expected Zend_Ldap_Exception not thrown');
+        } catch (Zend_Ldap_Exception $e) {
+            $this->assertEquals('Parameter $index must be an integer', $e->getMessage());
+        }
+        try {
+            $this->assertEquals(array('cn' => 'Baker, Alice'), $dn->get(-1));
+            $this->fail('Expected Zend_Ldap_Exception not thrown');
+        } catch (Zend_Ldap_Exception $e) {
+            $this->assertEquals('Parameter $index out of bounds', $e->getMessage());
+        }
+        try {
+            $this->assertEquals(array('dc' => 'com'), $dn->get(4));
+            $this->fail('Expected Zend_Ldap_Exception not thrown');
+        } catch (Zend_Ldap_Exception $e) {
+            $this->assertEquals('Parameter $index out of bounds', $e->getMessage());
+        }
+
+        $this->assertEquals(array(
+            array('cn' => 'Baker, Alice'),
+            array('cn' => 'Users', 'ou' => 'Lab')
+            ), $dn->get(0, 2));
+        $this->assertEquals(array(
+            array('cn' => 'Baker, Alice'),
+            array('cn' => 'Users', 'ou' => 'Lab'),
+            array('dc' => 'example')
+            ), $dn->get(0, 3));
+        $this->assertEquals(array(
+            array('cn' => 'Baker, Alice'),
+            array('cn' => 'Users', 'ou' => 'Lab'),
+            array('dc' => 'example'),
+            array('dc' => 'com')
+            ), $dn->get(0, 4));
+        $this->assertEquals(array(
+            array('cn' => 'Baker, Alice'),
+            array('cn' => 'Users', 'ou' => 'Lab'),
+            array('dc' => 'example'),
+            array('dc' => 'com')
+            ), $dn->get(0, 5));
+
+        $this->assertEquals(array(
+            array('cn' => 'Users', 'ou' => 'Lab'),
+            array('dc' => 'example')
+            ), $dn->get(1, 2));
+        $this->assertEquals(array(
+            array('cn' => 'Users', 'ou' => 'Lab'),
+            array('dc' => 'example'),
+            array('dc' => 'com')
+            ), $dn->get(1, 3));
+        $this->assertEquals(array(
+            array('cn' => 'Users', 'ou' => 'Lab'),
+            array('dc' => 'example'),
+            array('dc' => 'com')
+            ), $dn->get(1, 4));
+
+        $this->assertEquals(array(
+            array('dc' => 'example'),
+            array('dc' => 'com')
+            ), $dn->get(2, 2));
+        $this->assertEquals(array(
+            array('dc' => 'example'),
+            array('dc' => 'com')
+            ), $dn->get(2, 3));
+
+        $this->assertEquals(array(
+            array('dc' => 'com')
+            ), $dn->get(3, 2));
+    }
+
+    public function testDnManipulationSet()
+    {
+        $dnString='cn=Baker\\, Alice,cn=Users+ou=Lab,dc=example,dc=com';
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+
+        $this->assertEquals('uid=abaker,cn=Users+ou=Lab,dc=example,dc=com',
+            $dn->set(0, array('uid' => 'abaker'))->toString());
+        $this->assertEquals('uid=abaker,ou=Lab,dc=example,dc=com',
+            $dn->set(1, array('ou' => 'Lab'))->toString());
+        $this->assertEquals('uid=abaker,ou=Lab,dc=example+ou=Test,dc=com',
+            $dn->set(2, array('dc' => 'example', 'ou' => 'Test'))->toString());
+        $this->assertEquals('uid=abaker,ou=Lab,dc=example+ou=Test,dc=de\+fr',
+            $dn->set(3, array('dc' => 'de+fr'))->toString());
+
+        try {
+            $dn->set(4, array('dc' => 'de'));
+            $this->fail('Expected Zend_Ldap_Exception not thrown');
+        } catch (Zend_Ldap_Exception $e) {
+            $this->assertEquals('Parameter $index out of bounds', $e->getMessage());
+        }
+        try {
+            $dn->set(3, array('dc' => 'de', 'ou'));
+            $this->fail('Expected Zend_Ldap_Exception not thrown');
+        } catch (Zend_Ldap_Exception $e) {
+            $this->assertEquals('RDN Array is malformed: it must use string keys', $e->getMessage());
+        }
+    }
+
+    public function testDnManipulationRemove()
+    {
+        $dnString='cn=Baker\\, Alice,cn=Users+ou=Lab,dc=example,dc=com';
+
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+        $this->assertEquals('cn=Users+ou=Lab,dc=example,dc=com', $dn->remove(0)->toString());
+
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+        $this->assertEquals('cn=Baker\\, Alice,dc=example,dc=com', $dn->remove(1)->toString());
+
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+        $this->assertEquals('cn=Baker\\, Alice,cn=Users+ou=Lab,dc=com', $dn->remove(2)->toString());
+
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+        $this->assertEquals('cn=Baker\\, Alice,cn=Users+ou=Lab,dc=example',
+            $dn->remove(3)->toString());
+
+        try {
+            $dn=Zend_Ldap_Dn::fromString($dnString);
+            $dn->remove(4);
+            $this->fail('Expected Zend_Ldap_Exception not thrown');
+        } catch (Zend_Ldap_Exception $e) {
+            $this->assertEquals('Parameter $index out of bounds', $e->getMessage());
+        }
+
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+        $this->assertEquals('cn=Baker\\, Alice,dc=com',
+            $dn->remove(1, 2)->toString());
+
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+        $this->assertEquals('cn=Baker\\, Alice',
+            $dn->remove(1, 3)->toString());
+
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+        $this->assertEquals('cn=Baker\\, Alice',
+            $dn->remove(1, 4)->toString());
+    }
+
+    public function testDnManipulationAppendAndPrepend()
+    {
+        $dnString='OU=Sales,DC=example';
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+
+        $this->assertEquals('OU=Sales,DC=example,DC=com',
+            $dn->append(array('DC' => 'com'))->toString());
+
+        $this->assertEquals('OU=New York,OU=Sales,DC=example,DC=com',
+            $dn->prepend(array('OU' => 'New York'))->toString());
+
+        try {
+            $dn->append(array('dc' => 'de', 'ou'));
+            $this->fail('Expected Zend_Ldap_Exception not thrown');
+        } catch (Zend_Ldap_Exception $e) {
+            $this->assertEquals('RDN Array is malformed: it must use string keys', $e->getMessage());
+        }
+        try {
+            $dn->prepend(array('dc' => 'de', 'ou'));
+            $this->fail('Expected Zend_Ldap_Exception not thrown');
+        } catch (Zend_Ldap_Exception $e) {
+            $this->assertEquals('RDN Array is malformed: it must use string keys', $e->getMessage());
+        }
+    }
+
+    public function testDnManipulationInsert()
+    {
+        $dnString='cn=Baker\\, Alice,cn=Users,dc=example,dc=com';
+
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+        $this->assertEquals('cn=Baker\\, Alice,dc=test,cn=Users,dc=example,dc=com',
+            $dn->insert(0, array('dc' => 'test'))->toString());
+
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+        $this->assertEquals('cn=Baker\\, Alice,cn=Users,dc=test,dc=example,dc=com',
+            $dn->insert(1, array('dc' => 'test'))->toString());
+
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+        $this->assertEquals('cn=Baker\\, Alice,cn=Users,dc=example,dc=test,dc=com',
+            $dn->insert(2, array('dc' => 'test'))->toString());
+
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+        $this->assertEquals('cn=Baker\\, Alice,cn=Users,dc=example,dc=com,dc=test',
+            $dn->insert(3, array('dc' => 'test'))->toString());
+
+        try {
+            $dn=Zend_Ldap_Dn::fromString($dnString);
+            $dn->insert(4, array('dc' => 'de'));
+            $this->fail('Expected Zend_Ldap_Exception not thrown');
+        } catch (Zend_Ldap_Exception $e) {
+            $this->assertEquals('Parameter $index out of bounds', $e->getMessage());
+        }
+        try {
+            $dn=Zend_Ldap_Dn::fromString($dnString);
+            $dn->insert(3, array('dc' => 'de', 'ou'));
+            $this->fail('Expected Zend_Ldap_Exception not thrown');
+        } catch (Zend_Ldap_Exception $e) {
+            $this->assertEquals('RDN Array is malformed: it must use string keys', $e->getMessage());
+        }
+    }
+
+    public function testArrayAccessImplementation()
+    {
+        $dnString='cn=Baker\\, Alice,cn=Users,dc=example,dc=com';
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+
+        $this->assertEquals(array('cn' => 'Baker, Alice'), $dn[0]);
+        $this->assertEquals(array('cn' => 'Users'), $dn[1]);
+        $this->assertEquals(array('dc' => 'example'), $dn[2]);
+        $this->assertEquals(array('dc' => 'com'), $dn[3]);
+
+        $this->assertTrue(isset($dn[0]));
+        $this->assertTrue(isset($dn[1]));
+        $this->assertTrue(isset($dn[2]));
+        $this->assertTrue(isset($dn[3]));
+        $this->assertFalse(isset($dn[-1]));
+        $this->assertFalse(isset($dn[4]));
+
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+        unset($dn[0]);
+        $this->assertEquals('cn=Users,dc=example,dc=com', $dn->toString());
+
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+        unset($dn[1]);
+        $this->assertEquals('cn=Baker\\, Alice,dc=example,dc=com', $dn->toString());
+
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+        unset($dn[2]);
+        $this->assertEquals('cn=Baker\\, Alice,cn=Users,dc=com', $dn->toString());
+
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+        unset($dn[3]);
+        $this->assertEquals('cn=Baker\\, Alice,cn=Users,dc=example', $dn->toString());
+
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+        $dn[0]=array('uid' => 'abaker');
+        $this->assertEquals('uid=abaker,cn=Users,dc=example,dc=com', $dn->toString());
+
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+        $dn[1]=array('ou' => 'Lab');
+        $this->assertEquals('cn=Baker\\, Alice,ou=Lab,dc=example,dc=com', $dn->toString());
+
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+        $dn[2]=array('dc' => 'example', 'ou' => 'Test');
+        $this->assertEquals('cn=Baker\\, Alice,cn=Users,dc=example+ou=Test,dc=com', $dn->toString());
+
+        $dn=Zend_Ldap_Dn::fromString($dnString);
+        $dn[3]=array('dc' => 'de+fr');
+        $this->assertEquals('cn=Baker\\, Alice,cn=Users,dc=example,dc=de\+fr', $dn->toString());
+    }
+}

+ 216 - 0
tests/Zend/Ldap/FilterTest.php

@@ -0,0 +1,216 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Test helper
+ */
+require_once dirname(dirname(dirname(__FILE__))) . DIRECTORY_SEPARATOR . 'TestHelper.php';
+/**
+ * Zend_Ldap_Filter
+ */
+require_once 'Zend/Ldap/Filter.php';
+/**
+ * Zend_Ldap_Filter_And
+ */
+require_once 'Zend/Ldap/Filter/And.php';
+/**
+ * Zend_Ldap_Filter_Or
+ */
+require_once 'Zend/Ldap/Filter/Or.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+class Zend_Ldap_FilterTest extends PHPUnit_Framework_TestCase 
+{
+    public function testFilterEscapeBasicOperation()
+    {
+        $input = 'a*b(b)d\e/f';
+        $expected = 'a\2ab\28b\29d\5ce/f';
+        $this->assertEquals($expected, Zend_Ldap_Filter::escapeValue($input));
+    }
+    
+    public function testEscapeValues() 
+    {
+        $expected='t\28e,s\29t\2av\5cal\1eue';
+        $filterval='t(e,s)t*v\\al' . chr(30) . 'ue';
+        $this->assertEquals($expected, Zend_Ldap_Filter::escapeValue($filterval));
+        $this->assertEquals($expected, Zend_Ldap_Filter::escapeValue(array($filterval)));
+        $this->assertEquals(array($expected, $expected, $expected), 
+            Zend_Ldap_Filter::escapeValue(array($filterval, $filterval, $filterval)));
+    }
+
+    public function testUnescapeValues() 
+    {
+        $expected='t(e,s)t*v\\al' . chr(30) . 'ue';
+        $filterval='t\28e,s\29t\2av\5cal\1eue';
+        $this->assertEquals($expected, Zend_Ldap_Filter::unescapeValue($filterval));
+        $this->assertEquals($expected, Zend_Ldap_Filter::unescapeValue(array($filterval)));
+        $this->assertEquals(array($expected, $expected, $expected), 
+            Zend_Ldap_Filter::unescapeValue(array($filterval, $filterval, $filterval)));
+    }
+    
+    public function testFilterValueUtf8()
+    {
+        $filter='ÄÖÜäöü߀';
+        $escaped=Zend_Ldap_Filter::escapeValue($filter);
+        $unescaped=Zend_Ldap_Filter::unescapeValue($escaped);
+        $this->assertEquals($filter, $unescaped);
+    }
+    
+    public function testFilterCreation()
+    {
+        $f1=Zend_Ldap_Filter::equals('name', 'value');
+        $this->assertEquals('(name=value)', $f1->toString());
+        $f2=Zend_Ldap_Filter::begins('name', 'value');
+        $this->assertEquals('(name=value*)', $f2->toString());
+        $f3=Zend_Ldap_Filter::ends('name', 'value');
+        $this->assertEquals('(name=*value)', $f3->toString());
+        $f4=Zend_Ldap_Filter::contains('name', 'value');
+        $this->assertEquals('(name=*value*)', $f4->toString());
+        $f5=Zend_Ldap_Filter::greater('name', 'value');
+        $this->assertEquals('(name>value)', $f5->toString());
+        $f6=Zend_Ldap_Filter::greaterOrEqual('name', 'value');
+        $this->assertEquals('(name>=value)', $f6->toString());
+        $f7=Zend_Ldap_Filter::less('name', 'value');
+        $this->assertEquals('(name<value)', $f7->toString());
+        $f8=Zend_Ldap_Filter::lessOrEqual('name', 'value');
+        $this->assertEquals('(name<=value)', $f8->toString());
+        $f9=Zend_Ldap_Filter::approx('name', 'value');
+        $this->assertEquals('(name~=value)', $f9->toString());
+        $f10=Zend_Ldap_Filter::any('name');
+        $this->assertEquals('(name=*)', $f10->toString());
+        $f11=Zend_Ldap_Filter::string('name=*value*value*');
+        $this->assertEquals('(name=*value*value*)', $f11->toString());
+        $f12=Zend_Ldap_Filter::mask('(&(objectClass=account)(uid=%s))', 'a*b(b)d\e/f');
+        $this->assertEquals('(&(objectClass=account)(uid=a\2ab\28b\29d\5ce/f))', $f12->toString());
+    }
+    
+    public function testToStringImplementation()
+    {
+        $f1=Zend_Ldap_Filter::ends('name', 'value');
+        $this->assertEquals($f1->toString(), (string)$f1);
+    }
+    
+    public function testNegate()
+    {
+        $f1=Zend_Ldap_Filter::ends('name', 'value');
+        $this->assertEquals('(name=*value)', $f1->toString());
+        $f1=$f1->negate();
+        $this->assertEquals('(!(name=*value))', $f1->toString());
+        $f1=$f1->negate();
+        $this->assertEquals('(name=*value)', $f1->toString());
+    }
+    
+    /**
+     * @expectedException Zend_Ldap_Filter_Exception
+     */
+    public function testIllegalGroupingFilter()
+    {
+        $data=array('a', 'b', 5);
+        $f=new Zend_Ldap_Filter_And($data);
+    }
+    
+    public function testGroupingFilter()
+    {
+        $f1=Zend_Ldap_Filter::equals('name', 'value');
+        $f2=Zend_Ldap_Filter::begins('name', 'value');
+        $f3=Zend_Ldap_Filter::ends('name', 'value');
+        
+        $f4=Zend_Ldap_Filter::andFilter($f1, $f2, $f3);
+        $f5=Zend_Ldap_Filter::orFilter($f1, $f2, $f3);
+        
+        $this->assertEquals('(&(name=value)(name=value*)(name=*value))', $f4->toString());
+        $this->assertEquals('(|(name=value)(name=value*)(name=*value))', $f5->toString());
+        
+        $f4=$f4->addFilter($f1);
+        $this->assertEquals('(&(name=value)(name=value*)(name=*value)(name=value))', $f4->toString());
+    }
+    
+    public function testComplexFilter()
+    {
+        $f1=Zend_Ldap_Filter::equals('name1', 'value1');
+        $f2=Zend_Ldap_Filter::equals('name1', 'value2');
+        
+        $f3=Zend_Ldap_Filter::equals('name2', 'value1');
+        $f4=Zend_Ldap_Filter::equals('name2', 'value2');
+        
+        $f5=Zend_Ldap_Filter::orFilter($f1, $f2);
+        $f6=Zend_Ldap_Filter::orFilter($f3, $f4);
+        
+        $f7=Zend_Ldap_Filter::andFilter($f5, $f6);
+        
+        $this->assertEquals('(&(|(name1=value1)(name1=value2))(|(name2=value1)(name2=value2)))', 
+            $f7->toString());
+    }
+    
+    public function testChaining()
+    {
+        $f=Zend_Ldap_Filter::equals('a1', 'v1')
+            ->addAnd(Zend_Ldap_Filter::approx('a2', 'v2'));
+        $this->assertEquals('(&(a1=v1)(a2~=v2))', $f->toString());
+        $f=Zend_Ldap_Filter::equals('a1', 'v1')
+            ->addOr(Zend_Ldap_Filter::approx('a2', 'v2'));
+        $this->assertEquals('(|(a1=v1)(a2~=v2))', $f->toString());
+        $f=Zend_Ldap_Filter::equals('a1', 'v1')
+            ->negate()
+            ->addOr(Zend_Ldap_Filter::approx('a2', 'v2'));
+        $this->assertEquals('(|(!(a1=v1))(a2~=v2))', $f->toString());
+        $f=Zend_Ldap_Filter::equals('a1', 'v1')
+            ->addAnd(Zend_Ldap_Filter::approx('a2', 'v2')->negate());
+        $this->assertEquals('(&(a1=v1)(!(a2~=v2)))', $f->toString());
+        $f=Zend_Ldap_Filter::equals('a1', 'v1')
+            ->negate()
+            ->addAnd(Zend_Ldap_Filter::approx('a2', 'v2')->negate());
+        $this->assertEquals('(&(!(a1=v1))(!(a2~=v2)))', $f->toString());
+        $f=Zend_Ldap_Filter::equals('a1', 'v1')
+            ->negate()
+            ->addAnd(Zend_Ldap_Filter::approx('a2', 'v2')->negate());
+        $this->assertEquals('(&(!(a1=v1))(!(a2~=v2)))', $f->toString());
+        $f=Zend_Ldap_Filter::equals('a1', 'v1')
+            ->negate()
+            ->addAnd(Zend_Ldap_Filter::approx('a2', 'v2')->negate())
+            ->negate();
+        $this->assertEquals('(!(&(!(a1=v1))(!(a2~=v2))))', $f->toString());
+    }
+    
+    public function testRealFilterString()
+    {
+        $f1=Zend_Ldap_Filter::orFilter(
+            Zend_Ldap_Filter::equals('sn', 'Gehrig'), 
+            Zend_Ldap_Filter::equals('sn', 'Goerke')
+        );
+        $f2=Zend_Ldap_Filter::orFilter(
+            Zend_Ldap_Filter::equals('givenName', 'Stefan'), 
+            Zend_Ldap_Filter::equals('givenName', 'Ingo')
+        );
+        
+        $f=Zend_Ldap_Filter::andFilter($f1, $f2);
+        
+        $this->assertEquals('(&(|(sn=Gehrig)(sn=Goerke))(|(givenName=Stefan)(givenName=Ingo)))', 
+            $f->toString());   
+    }
+}

+ 86 - 0
tests/Zend/Ldap/Ldif/AllTests.php

@@ -0,0 +1,86 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * Test helper
+ */
+require_once dirname(dirname(dirname(dirname(__FILE__)))) . DIRECTORY_SEPARATOR . 'TestHelper.php';
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_Ldap_Ldif_AllTests::main');
+}
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Ldif_AllTests
+{
+    public static function main()
+    {
+        PHPUnit_TextUI_TestRunner::run(self::suite());
+    }
+
+    public static function suite()
+    {
+        $suite = new PHPUnit_Framework_TestSuite('Zend Framework - Zend_Ldap_Ldif');
+
+        /**
+         * @see Zend_Ldap_Ldif_SimpleEncoderTest
+         */
+        require_once 'Zend/Ldap/Ldif/SimpleEncoderTest.php';
+        $suite->addTestSuite('Zend_Ldap_Ldif_SimpleEncoderTest');
+        /**
+         * @see Zend_Ldap_Ldif_SimpleDecoderTest
+         */
+        require_once 'Zend/Ldap/Ldif/SimpleDecoderTest.php';
+        $suite->addTestSuite('Zend_Ldap_Ldif_SimpleDecoderTest');
+
+        if (defined('TESTS_ZEND_LDAP_ONLINE_ENABLED')
+                && constant('TESTS_ZEND_LDAP_ONLINE_ENABLED')) {
+
+        } else {
+            $suite->addTest(new Zend_Ldap_Ldif_SkipOnlineTests());
+        }
+
+        return $suite;
+    }
+}
+
+class Zend_Ldap_Ldif_SkipOnlineTests extends PHPUnit_Framework_TestCase
+{
+    public function setUp()
+    {
+        $this->markTestSkipped('Zend_Ldap_Ldif online tests not enabled in TestConfiguration.php');
+    }
+
+    public function testNothing()
+    {
+    }
+}
+
+if (PHPUnit_MAIN_METHOD == 'Zend_Ldap_Ldif_AllTests::main') {
+    Zend_Ldap_Ldif_AllTests::main();
+}

+ 388 - 0
tests/Zend/Ldap/Ldif/SimpleDecoderTest.php

@@ -0,0 +1,388 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Zend_Ldap_TestCase
+ */
+require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'TestCase.php';
+/**
+ * @see Zend_Ldap_Ldif_Encoder
+ */
+require_once 'Zend/Ldap/Ldif/Encoder.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+class Zend_Ldap_Ldif_SimpleDecoderTest extends Zend_Ldap_TestCase
+{
+    public function testDecodeSimpleSingleItem()
+    {
+        $data =
+"version: 1
+dn: cn=test3,ou=example,dc=cno
+objectclass: oc1
+attr3: foo";
+        $expected = array(
+            'dn'          => 'cn=test3,ou=example,dc=cno',
+            'objectclass' => array('oc1'),
+            'attr3'       => array('foo'));
+        $actual = Zend_Ldap_Ldif_Encoder::decode($data);
+        $this->assertEquals($expected, $actual);
+    }
+
+    public function testDecodeSingleItemWithFoldedAttribute()
+    {
+        $data =
+"dn: cn=test blabla,ou=example,dc=cno
+objectclass: oc2
+attr1: 12345
+attr2: 1234
+attr2: baz
+attr3: foo
+attr3: bar
+cn: test blabla
+verylong: fhu08rhvt7b478vt5hv78h45nfgt45h78t34hhhhhhhhhv5bg8
+ h6ttttttttt3489t57nhvgh4788trhg8999vnhtgthgui65hgb
+ 5789thvngwr789cghm738";
+        $expected = array(
+            'dn'          => 'cn=test blabla,ou=example,dc=cno',
+            'objectclass' => array('oc2'),
+            'attr1'       => array('12345'),
+            'attr2'       => array('1234', 'baz'),
+            'attr3'       => array('foo', 'bar'),
+            'cn'          => array('test blabla'),
+            'verylong'    => array('fhu08rhvt7b478vt5hv78h45nfgt45h78t34hhhhhhhhhv5bg8' .
+                                    'h6ttttttttt3489t57nhvgh4788trhg8999vnhtgthgui65hgb' .
+                                    '5789thvngwr789cghm738'),
+        );
+        $actual = Zend_Ldap_Ldif_Encoder::decode($data);
+        $this->assertEquals($expected, $actual);
+    }
+
+    public function testDecodeSingleItemWithBase64Attributes()
+    {
+        $data =
+"dn:: Y249dGVzdCBibGFibGEsb3U9ZXhhbXBsZSxkYz1jbm8=
+objectclass: oc3
+attr1: 12345
+attr2: 1234
+attr2: baz
+attr3: foo
+attr3: bar
+attr4:: w7bDpMO8
+attr5:: ZW5kc3BhY2Ug
+attr6:: OmJhZGluaXRjaGFy
+attr6:: PGJhZGluaXRjaGFy
+cn:: dGVzdCDDtsOkw7w=";
+        $expected = array(
+            'dn'          => 'cn=test blabla,ou=example,dc=cno',
+            'objectclass' => array('oc3'),
+            'attr1'       => array('12345'),
+            'attr2'       => array('1234', 'baz'),
+            'attr3'       => array('foo', 'bar'),
+            'attr4'       => array('öäü'),
+            'attr5'       => array('endspace '),
+            'attr6'       => array(':badinitchar', '<badinitchar'),
+            'cn'          => array('test öäü'),
+        );
+        $actual = Zend_Ldap_Ldif_Encoder::decode($data);
+        $this->assertEquals($expected, $actual);
+    }
+
+    public function testDecodeSingleItemWithFoldedBase64Attribute()
+    {
+        $data =
+"dn:: Y249dGVzdCBibGFibGEsb
+ 3U9ZXhhbXBsZSxkYz1jbm8=
+objectclass: oc3
+attr1: 12345
+attr2: 1234
+attr2: baz
+attr3: foo
+attr3: bar";
+        $expected = array(
+            'dn'          => 'cn=test blabla,ou=example,dc=cno',
+            'objectclass' => array('oc3'),
+            'attr1'       => array('12345'),
+            'attr2'       => array('1234', 'baz'),
+            'attr3'       => array('foo', 'bar'),
+        );
+        $actual = Zend_Ldap_Ldif_Encoder::decode($data);
+        $this->assertEquals($expected, $actual);
+    }
+
+    public function testDecodeTwoItems()
+    {
+        $data =
+"version: 1
+dn: cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+cn: Barbara Jensen
+cn: Barbara J Jensen
+cn: Babs Jensen
+sn: Jensen
+uid: bjensen
+telephonenumber: +1 408 555 1212
+description: A big sailing fan.
+
+dn: cn=Bjorn Jensen, ou=Accounting, dc=airius, dc=com
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+cn: Bjorn Jensen
+sn: Jensen
+telephonenumber: +1 408 555 1212";
+        $expected = array(
+            array(
+                'dn'              => 'cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com',
+                'objectclass'     => array('top', 'person', 'organizationalPerson'),
+                'cn'              => array('Barbara Jensen', 'Barbara J Jensen', 'Babs Jensen'),
+                'sn'              => array('Jensen'),
+                'uid'             => array('bjensen'),
+                'telephonenumber' => array('+1 408 555 1212'),
+                'description'     => array('A big sailing fan.'),
+            ),
+            array(
+                'dn'              => 'cn=Bjorn Jensen, ou=Accounting, dc=airius, dc=com',
+                'objectclass'     => array('top', 'person', 'organizationalPerson'),
+                'cn'              => array('Bjorn Jensen'),
+                'sn'              => array('Jensen'),
+                'telephonenumber' => array('+1 408 555 1212'),
+            ),
+        );
+        $actual = Zend_Ldap_Ldif_Encoder::decode($data);
+        $this->assertEquals($expected, $actual);
+    }
+
+    public function testDecodeStringContainingEntryWithFoldedAttributeValue()
+    {
+        $data =
+"version: 1
+dn:cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com
+objectclass:top
+objectclass:person
+objectclass:organizationalPerson
+cn:Barbara Jensen
+cn:Barbara J Jensen
+cn:Babs Jensen
+sn:Jensen
+uid:bjensen
+telephonenumber:+1 408 555 1212
+description:Babs is a big sailing fan, and travels extensively in sea
+ rch of perfect sailing conditions.
+title:Product Manager, Rod and Reel Division";
+        $expected = array(
+            'dn'              => 'cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com',
+            'objectclass'     => array('top', 'person', 'organizationalPerson'),
+            'cn'              => array('Barbara Jensen', 'Barbara J Jensen', 'Babs Jensen'),
+            'sn'              => array('Jensen'),
+            'uid'             => array('bjensen'),
+            'telephonenumber' => array('+1 408 555 1212'),
+            'description'     => array('Babs is a big sailing fan, and travels extensively' .
+                                        ' in search of perfect sailing conditions.'),
+            'title'             => array('Product Manager, Rod and Reel Division'),
+        );
+        $actual = Zend_Ldap_Ldif_Encoder::decode($data);
+        $this->assertEquals($expected, $actual);
+    }
+
+    public function testDecodeStringContainingBase64EncodedValue()
+    {
+        $data =
+"version: 1
+dn: cn=Gern Jensen, ou=Product Testing, dc=airius, dc=com
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+cn: Gern Jensen
+cn: Gern O Jensen
+sn: Jensen
+uid: gernj
+telephonenumber: +1 408 555 1212
+description:: V2hhdCBhIGNhcmVmdWwgcmVhZGVyIHlvdSBhcmUhICBUaGlzIHZhbHVl
+ IGlzIGJhc2UtNjQtZW5jb2RlZCBiZWNhdXNlIGl0IGhhcyBhIGNvbnRyb2wgY2hhcmFjdG
+ VyIGluIGl0IChhIENSKS4NICBCeSB0aGUgd2F5LCB5b3Ugc2hvdWxkIHJlYWxseSBnZXQg
+ b3V0IG1vcmUu";
+        $expected = array(
+            'dn'              => 'cn=Gern Jensen, ou=Product Testing, dc=airius, dc=com',
+            'objectclass'     => array('top', 'person', 'organizationalPerson'),
+            'cn'              => array('Gern Jensen', 'Gern O Jensen'),
+            'sn'              => array('Jensen'),
+            'uid'             => array('gernj'),
+            'telephonenumber' => array('+1 408 555 1212'),
+            'description'     => array('What a careful reader you are!' .
+                                        '  This value is base-64-encoded because it has a ' .
+                                        'control character in it (a CR).' . "\r" .
+                                        '  By the way, you should really get out more.'),
+        );
+        $actual = Zend_Ldap_Ldif_Encoder::decode($data);
+        $this->assertEquals($expected, $actual);
+    }
+
+    public function testDecodeStringContainingEntriesWithUtf8EncodedAttributeValues()
+    {
+        $data =
+"version: 1
+dn:: b3U95Za25qWt6YOoLG89QWlyaXVz
+# dn:: ou=営業部,o=Airius
+objectclass: top
+objectclass: organizationalUnit
+ou:: 5Za25qWt6YOo
+# ou:: 営業部
+ou;lang-ja:: 5Za25qWt6YOo
+# ou;lang-ja:: 営業部
+ou;lang-ja;phonetic:: 44GI44GE44GO44KH44GG44G2
+# ou;lang-ja:: えいぎょうぶ
+
+ou;lang-en: Sales
+description: Japanese office
+
+dn:: dWlkPXJvZ2FzYXdhcmEsb3U95Za25qWt6YOoLG89QWlyaXVz
+# dn:: uid=rogasawara,ou=営業部,o=Airius
+userpassword: {SHA}O3HSv1MusyL4kTjP+HKI5uxuNoM=
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+uid: rogasawara
+mail: rogasawara@airius.co.jp
+givenname;lang-ja:: 44Ot44OJ44OL44O8
+# givenname;lang-ja:: ロドニー
+sn;lang-ja:: 5bCP56yg5Y6f
+# sn;lang-ja:: 小笠原
+cn;lang-ja:: 5bCP56yg5Y6fIOODreODieODi+ODvA==
+# cn;lang-ja:: 小笠原 ロドニー
+title;lang-ja:: 5Za25qWt6YOoIOmDqOmVtw==
+# title;lang-ja:: 営業部 部長
+preferredlanguage: ja
+givenname:: 44Ot44OJ44OL44O8
+# givenname:: ロドニー
+sn:: 5bCP56yg5Y6f
+# sn:: 小笠原
+cn:: 5bCP56yg5Y6fIOODreODieODi+ODvA==
+# cn:: 小笠原 ロドニー
+title:: 5Za25qWt6YOoIOmDqOmVtw==
+# title:: 営業部 部長
+givenname;lang-ja;phonetic:: 44KN44Gp44Gr44O8
+# givenname;lang-ja;phonetic:: ろどにー
+sn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJ
+# sn;lang-ja;phonetic:: おがさわら
+cn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJIOOCjeOBqeOBq+ODvA==
+# cn;lang-ja;phonetic:: おがさわら ろどにー
+title;lang-ja;phonetic:: 44GI44GE44GO44KH44GG44G2IOOBtuOBoeOCh+OBhg==
+# title;lang-ja;phonetic:: えいぎょうぶ ぶちょう
+givenname;lang-en: Rodney
+sn;lang-en: Ogasawara
+cn;lang-en: Rodney Ogasawara
+title;lang-en: Sales, Director";
+
+        $actual = Zend_Ldap_Ldif_Encoder::decode($data);
+
+        $this->assertEquals('ou=営業部,o=Airius', $actual[0]['dn']);
+        $this->assertEquals(array('top', 'organizationalUnit'), $actual[0]['objectclass']);
+        $this->assertEquals('営業部', $actual[0]['ou'][0]);
+        $this->assertEquals('営業部', $actual[0]['ou;lang-ja'][0]);
+        $this->assertEquals('えいぎょうぶ', $actual[0]['ou;lang-ja;phonetic'][0]);
+        $this->assertEquals('Sales', $actual[0]['ou;lang-en'][0]);
+        $this->assertEquals('Japanese office', $actual[0]['description'][0]);
+
+        $this->assertEquals('uid=rogasawara,ou=営業部,o=Airius', $actual[1]['dn']);
+        $this->assertEquals('{SHA}O3HSv1MusyL4kTjP+HKI5uxuNoM=', $actual[1]['userpassword'][0]);
+        $this->assertEquals(array('top', 'person', 'organizationalPerson', 'inetOrgPerson'),
+            $actual[1]['objectclass']);
+        $this->assertEquals('rogasawara', $actual[1]['uid'][0]);
+        $this->assertEquals('rogasawara@airius.co.jp', $actual[1]['mail'][0]);
+        $this->assertEquals('ロドニー', $actual[1]['givenname;lang-ja'][0]);
+        $this->assertEquals('小笠原', $actual[1]['sn;lang-ja'][0]);
+        $this->assertEquals('小笠原 ロドニー', $actual[1]['cn;lang-ja'][0]);
+        $this->assertEquals('営業部 部長', $actual[1]['title;lang-ja'][0]);
+        $this->assertEquals('ja', $actual[1]['preferredlanguage'][0]);
+        $this->assertEquals('ロドニー', $actual[1]['givenname'][0]);
+        $this->assertEquals('小笠原', $actual[1]['sn'][0]);
+        $this->assertEquals('小笠原 ロドニー', $actual[1]['cn'][0]);
+        $this->assertEquals('営業部 部長', $actual[1]['title'][0]);
+        $this->assertEquals('ろどにー', $actual[1]['givenname;lang-ja;phonetic'][0]);
+        $this->assertEquals('おがさわら', $actual[1]['sn;lang-ja;phonetic'][0]);
+        $this->assertEquals('おがさわら ろどにー', $actual[1]['cn;lang-ja;phonetic'][0]);
+        $this->assertEquals('えいぎょうぶ ぶちょう', $actual[1]['title;lang-ja;phonetic'][0]);
+        $this->assertEquals('Rodney', $actual[1]['givenname;lang-en'][0]);
+        $this->assertEquals('Ogasawara', $actual[1]['sn;lang-en'][0]);
+        $this->assertEquals('Rodney Ogasawara', $actual[1]['cn;lang-en'][0]);
+        $this->assertEquals('Sales, Director', $actual[1]['title;lang-en'][0]);
+    }
+
+    public function testDecodeSingleItemWithFoldedAttributesAndEmptyLinesBetween()
+    {
+        $data =
+"dn: cn=test blabla,ou=example,dc=cno
+
+objectclass: top
+
+
+objectclass: person
+
+objectclass: organizationalPerson
+
+description:: V2hhdCBhIGNhcmVmdWwgcmVhZGVyIHlvdSBhcmUhICBUaGlzIHZhbHVl
+
+ IGlzIGJhc2UtNjQtZW5jb2RlZCBiZWNhdXNlIGl0IGhhcyBhIGNvbnRyb2wgY2hhcmFjdG
+
+ VyIGluIGl0IChhIENSKS4NICBCeSB0aGUgd2F5LCB5b3Ugc2hvdWxkIHJlYWxseSBnZXQg
+
+ b3V0IG1vcmUu
+
+
+verylong: fhu08rhvt7b478vt5hv78h45nfgt45h78t34hhhhhhhhhv5bg8
+
+ h6ttttttttt3489t57nhvgh4788trhg8999vnhtgthgui65hgb
+
+ 5789thvngwr789cghm738";
+        $expected = array(
+            'dn'          => 'cn=test blabla,ou=example,dc=cno',
+            'objectclass' => array('top', 'person', 'organizationalPerson'),
+            'description' => array('What a careful reader you are!' .
+                                    '  This value is base-64-encoded because it has a ' .
+                                    'control character in it (a CR).' . "\r" .
+                                    '  By the way, you should really get out more.'),
+            'verylong'    => array('fhu08rhvt7b478vt5hv78h45nfgt45h78t34hhhhhhhhhv5bg8' .
+                                    'h6ttttttttt3489t57nhvgh4788trhg8999vnhtgthgui65hgb' .
+                                    '5789thvngwr789cghm738'),
+        );
+        $actual = Zend_Ldap_Ldif_Encoder::decode($data);
+        $this->assertEquals($expected, $actual);
+    }
+
+    public function testRoundtripEncoding()
+    {
+        $node = $this->_createTestNode();
+        $ldif = $node->toLdif();
+        $data = Zend_Ldap_Ldif_Encoder::decode($ldif);
+        $expected = array_merge(array('dn' => $node->getDnString()), $node->getData(false));
+        $this->assertEquals($expected, $data);
+    }
+}

+ 267 - 0
tests/Zend/Ldap/Ldif/SimpleEncoderTest.php

@@ -0,0 +1,267 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Zend_Ldap_TestCase
+ */
+require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'TestCase.php';
+/**
+ * @see Zend_Ldap_Ldif_Encoder
+ */
+require_once 'Zend/Ldap/Ldif/Encoder.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+class Zend_Ldap_Ldif_SimpleEncoderTest extends Zend_Ldap_TestCase
+{
+    public static function stringEncodingProvider()
+    {
+        $testData = array(
+            array('cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com',
+                'cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com'),
+            array('Babs is a big sailing fan, and travels extensively in search of perfect sailing conditions.',
+                'Babs is a big sailing fan, and travels extensively in search of perfect sailing conditions.'),
+            array("\x00 NULL CHAR first", base64_encode("\x00 NULL CHAR first")),
+            array("\n LF CHAR first", base64_encode("\n LF CHAR first")),
+            array("\r CR CHAR first", base64_encode("\r CR CHAR first")),
+            array(' SPACE CHAR first', base64_encode(' SPACE CHAR first')),
+            array(': colon CHAR first', base64_encode(': colon CHAR first')),
+            array('< less-than CHAR first', base64_encode('< less-than CHAR first')),
+            array("\x7f CHR(127) first", base64_encode("\x7f CHR(127) first")),
+            array("NULL CHAR \x00 in string", base64_encode("NULL CHAR \x00 in string")),
+            array("LF CHAR \n in string", base64_encode("LF CHAR \n in string")),
+            array("CR CHAR \r in string", base64_encode("CR CHAR \r in string")),
+            array("CHR(127) \x7f in string", base64_encode("CHR(127) \x7f in string")),
+            array('Ä first', base64_encode('Ä first')),
+            array('in Ä string', base64_encode('in Ä string')),
+            array('last char is a string ', base64_encode('last char is a string '))
+        );
+        return $testData;
+    }
+
+    /**
+     * @dataProvider stringEncodingProvider
+     */
+    public function testStringEncoding($string, $expected)
+    {
+        $this->assertEquals($expected, Zend_Ldap_Ldif_Encoder::encode($string));
+    }
+
+    public static function attributeEncodingProvider()
+    {
+        $testData = array(
+            array(array('dn' => 'cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com'),
+                'dn: cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com'),
+            array(array('dn' => 'cn=Jürgen Österreicher, ou=Äpfel, dc=airius, dc=com'),
+                'dn:: ' . base64_encode('cn=Jürgen Österreicher, ou=Äpfel, dc=airius, dc=com')),
+            array(array('description' => 'Babs is a big sailing fan, and travels extensively in search of perfect sailing conditions.'),
+                'description: Babs is a big sailing fan, and travels extensively in search of p' . PHP_EOL . ' erfect sailing conditions.'),
+            array(array('description' => "CHR(127) \x7f in string"),
+                'description:: ' . base64_encode("CHR(127) \x7f in string")),
+            array(array('description' => '1234567890123456789012345678901234567890123456789012345678901234 567890'),
+                'description: 1234567890123456789012345678901234567890123456789012345678901234 ' . PHP_EOL . ' 567890'),
+        );
+        return $testData;
+    }
+
+    /**
+     * @dataProvider attributeEncodingProvider
+     */
+    public function testAttributeEncoding($array, $expected)
+    {
+        $actual = Zend_Ldap_Ldif_Encoder::encode($array);
+        $this->assertEquals($expected, $actual);
+    }
+
+    public function testChangedWrapCount()
+    {
+        $input = '56789012345678901234567890';
+        $expected = 'dn: 567890' . PHP_EOL . ' 1234567890' . PHP_EOL . ' 1234567890';
+        $output = Zend_Ldap_Ldif_Encoder::encode(array('dn' => $input), array('wrap' => 10));
+        $this->assertEquals($expected, $output);
+    }
+
+    public function testEncodeMultipleAttributes()
+    {
+        $data = array(
+            'a' => array('a', 'b'),
+            'b' => 'c',
+            'c' => '',
+            'd' => array(),
+            'e' => array(''));
+        $expected = 'a: a' . PHP_EOL .
+            'a: b' . PHP_EOL .
+            'b: c' . PHP_EOL .
+            'c: ' . PHP_EOL .
+            'd: ' . PHP_EOL .
+            'e: ';
+        $actual = Zend_Ldap_Ldif_Encoder::encode($data);
+        $this->assertEquals($expected, $actual);
+    }
+
+    public function testEncodeUnsupportedType()
+    {
+        $this->assertNull(Zend_Ldap_Ldif_Encoder::encode(new stdClass()));
+    }
+
+    public function testSorting()
+    {
+        $data=array(
+            'cn'          => array('name'),
+            'dn'          => 'cn=name,dc=example,dc=org',
+            'host'        => array('a', 'b', 'c'),
+            'empty'       => array(),
+            'boolean'     => array('TRUE', 'FALSE'),
+            'objectclass' => array('account', 'top'),
+        );
+        $expected = 'version: 1' . PHP_EOL .
+            'dn: cn=name,dc=example,dc=org' . PHP_EOL .
+            'objectclass: account' . PHP_EOL .
+            'objectclass: top' . PHP_EOL .
+            'boolean: TRUE' . PHP_EOL .
+            'boolean: FALSE' . PHP_EOL .
+            'cn: name' . PHP_EOL .
+            'empty: ' . PHP_EOL .
+            'host: a' . PHP_EOL .
+            'host: b' . PHP_EOL .
+            'host: c';
+        $actual = Zend_Ldap_Ldif_Encoder::encode($data);
+        $this->assertEquals($expected, $actual);
+
+        $expected = 'version: 1' . PHP_EOL .
+            'cn: name' . PHP_EOL .
+            'dn: cn=name,dc=example,dc=org' . PHP_EOL .
+            'host: a' . PHP_EOL .
+            'host: b' . PHP_EOL .
+            'host: c' . PHP_EOL .
+            'empty: ' . PHP_EOL .
+            'boolean: TRUE' . PHP_EOL .
+            'boolean: FALSE' . PHP_EOL .
+            'objectclass: account' . PHP_EOL .
+            'objectclass: top';
+        $actual = Zend_Ldap_Ldif_Encoder::encode($data, array('sort' => false));
+        $this->assertEquals($expected, $actual);
+    }
+
+    public function testNodeEncoding()
+    {
+        $node = $this->_createTestNode();
+        $expected = 'version: 1' . PHP_EOL .
+            'dn: cn=name,dc=example,dc=org' . PHP_EOL .
+            'objectclass: account' . PHP_EOL .
+            'objectclass: top' . PHP_EOL .
+            'boolean: TRUE' . PHP_EOL .
+            'boolean: FALSE' . PHP_EOL .
+            'cn: name' . PHP_EOL .
+            'empty: ' . PHP_EOL .
+            'host: a' . PHP_EOL .
+            'host: b' . PHP_EOL .
+            'host: c';
+        $actual = $node->toLdif();
+        $this->assertEquals($expected, $actual);
+
+        $actual = Zend_Ldap_Ldif_Encoder::encode($node);
+        $this->assertEquals($expected, $actual);
+    }
+
+    public function testSupressVersionHeader()
+    {
+        $data=array(
+            'cn'          => array('name'),
+            'dn'          => 'cn=name,dc=example,dc=org',
+            'host'        => array('a', 'b', 'c'),
+            'empty'       => array(),
+            'boolean'     => array('TRUE', 'FALSE'),
+            'objectclass' => array('account', 'top'),
+        );
+        $expected = 'dn: cn=name,dc=example,dc=org' . PHP_EOL .
+            'objectclass: account' . PHP_EOL .
+            'objectclass: top' . PHP_EOL .
+            'boolean: TRUE' . PHP_EOL .
+            'boolean: FALSE' . PHP_EOL .
+            'cn: name' . PHP_EOL .
+            'empty: ' . PHP_EOL .
+            'host: a' . PHP_EOL .
+            'host: b' . PHP_EOL .
+            'host: c';
+        $actual = Zend_Ldap_Ldif_Encoder::encode($data, array('version' => null));
+        $this->assertEquals($expected, $actual);
+    }
+
+    public function testEncodingWithJapaneseCharacters()
+    {
+        $data=array(
+            'dn'                         => 'uid=rogasawara,ou=営業部,o=Airius',
+            'objectclass'                => array('top', 'person', 'organizationalPerson', 'inetOrgPerson'),
+            'uid'                        => array('rogasawara'),
+            'mail'                       => array('rogasawara@airius.co.jp'),
+            'givenname;lang-ja'          => array('ロドニー'),
+            'sn;lang-ja'                 => array('小笠原'),
+            'cn;lang-ja'                 => array('小笠原 ロドニー'),
+            'title;lang-ja'              => array('営業部 部長'),
+            'preferredlanguage'          => array('ja'),
+            'givenname'                  => array('ロドニー'),
+            'sn'                         => array('小笠原'),
+            'cn'                         => array('小笠原 ロドニー'),
+            'title'                      => array('営業部 部長'),
+            'givenname;lang-ja;phonetic' => array('ろどにー'),
+            'sn;lang-ja;phonetic'        => array('おがさわら'),
+            'cn;lang-ja;phonetic'        => array('おがさわら ろどにー'),
+            'title;lang-ja;phonetic'     => array('えいぎょうぶ ぶちょう'),
+            'givenname;lang-en'          => array('Rodney'),
+            'sn;lang-en'                 => array('Ogasawara'),
+            'cn;lang-en'                 => array('Rodney Ogasawara'),
+            'title;lang-en'              => array('Sales, Director'),
+        );
+        $expected = 'dn:: dWlkPXJvZ2FzYXdhcmEsb3U95Za25qWt6YOoLG89QWlyaXVz' . PHP_EOL .
+            'objectclass: top' . PHP_EOL .
+            'objectclass: person' . PHP_EOL .
+            'objectclass: organizationalPerson' . PHP_EOL .
+            'objectclass: inetOrgPerson' . PHP_EOL .
+            'uid: rogasawara' . PHP_EOL .
+            'mail: rogasawara@airius.co.jp' . PHP_EOL .
+            'givenname;lang-ja:: 44Ot44OJ44OL44O8' . PHP_EOL .
+            'sn;lang-ja:: 5bCP56yg5Y6f' . PHP_EOL .
+            'cn;lang-ja:: 5bCP56yg5Y6fIOODreODieODi+ODvA==' . PHP_EOL .
+            'title;lang-ja:: 5Za25qWt6YOoIOmDqOmVtw==' . PHP_EOL .
+            'preferredlanguage: ja' . PHP_EOL .
+            'givenname:: 44Ot44OJ44OL44O8' . PHP_EOL .
+            'sn:: 5bCP56yg5Y6f' . PHP_EOL .
+            'cn:: 5bCP56yg5Y6fIOODreODieODi+ODvA==' . PHP_EOL .
+            'title:: 5Za25qWt6YOoIOmDqOmVtw==' . PHP_EOL .
+            'givenname;lang-ja;phonetic:: 44KN44Gp44Gr44O8' . PHP_EOL .
+            'sn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJ' . PHP_EOL .
+            'cn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJIOOCjeOBqeOBq+ODvA==' . PHP_EOL .
+            'title;lang-ja;phonetic:: 44GI44GE44GO44KH44GG44G2IOOBtuOBoeOCh+OBhg==' . PHP_EOL .
+            'givenname;lang-en: Rodney' . PHP_EOL .
+            'sn;lang-en: Ogasawara' . PHP_EOL .
+            'cn;lang-en: Rodney Ogasawara' . PHP_EOL .
+            'title;lang-en: Sales, Director';
+        $actual = Zend_Ldap_Ldif_Encoder::encode($data, array('sort' => false, 'version' => null));
+        $this->assertEquals($expected, $actual);
+    }
+}

+ 115 - 0
tests/Zend/Ldap/Node/AllTests.php

@@ -0,0 +1,115 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * Test helper
+ */
+require_once dirname(dirname(dirname(dirname(__FILE__)))) . DIRECTORY_SEPARATOR . 'TestHelper.php';
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_Ldap_Node_AllTests::main');
+}
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_Node_AllTests
+{
+    public static function main()
+    {
+        PHPUnit_TextUI_TestRunner::run(self::suite());
+    }
+
+    public static function suite()
+    {
+        $suite = new PHPUnit_Framework_TestSuite('Zend Framework - Zend_Ldap_Node');
+
+        /**
+         * @see Zend_Ldap_Node_OfflineTest
+         */
+        require_once 'Zend/Ldap/Node/OfflineTest.php';
+        $suite->addTestSuite('Zend_Ldap_Node_OfflineTest');
+        /**
+         * @see Zend_Ldap_Node_AttributeIterationTest
+         */
+        require_once 'Zend/Ldap/Node/AttributeIterationTest.php';
+        $suite->addTestSuite('Zend_Ldap_Node_AttributeIterationTest');
+
+        if (defined('TESTS_ZEND_LDAP_ONLINE_ENABLED')
+                && constant('TESTS_ZEND_LDAP_ONLINE_ENABLED')) {
+            /**
+             * @see Zend_Ldap_Node_OnlineTest
+             */
+            require_once 'Zend/Ldap/Node/OnlineTest.php';
+            $suite->addTestSuite('Zend_Ldap_Node_OnlineTest');
+            /**
+             * @see Zend_Ldap_Node_ChildrenTest
+             */
+            require_once 'Zend/Ldap/Node/ChildrenTest.php';
+            $suite->addTestSuite('Zend_Ldap_Node_ChildrenTest');
+            /**
+             * @see Zend_Ldap_Node_ChildrenIterationTest
+             */
+            require_once 'Zend/Ldap/Node/ChildrenIterationTest.php';
+            $suite->addTestSuite('Zend_Ldap_Node_ChildrenIterationTest');
+            /**
+             * @see Zend_Ldap_Node_UpdateTest
+             */
+            require_once 'Zend/Ldap/Node/UpdateTest.php';
+            $suite->addTestSuite('Zend_Ldap_Node_UpdateTest');
+            /**
+             * @see Zend_Ldap_Node_RootDseTest
+             */
+            require_once 'Zend/Ldap/Node/RootDseTest.php';
+            $suite->addTestSuite('Zend_Ldap_Node_RootDseTest');
+            /**
+             * @see Zend_Ldap_Node_SchemaTest
+             */
+            require_once 'Zend/Ldap/Node/SchemaTest.php';
+            $suite->addTestSuite('Zend_Ldap_Node_SchemaTest');
+        } else {
+            $suite->addTest(new Zend_Ldap_Node_SkipOnlineTests());
+        }
+
+        return $suite;
+    }
+}
+
+class Zend_Ldap_Node_SkipOnlineTests extends PHPUnit_Framework_TestCase
+{
+    public function setUp()
+    {
+        $this->markTestSkipped('Zend_Ldap_Node online tests not enabled in TestConfiguration.php');
+    }
+
+    public function testNothing()
+    {
+    }
+}
+
+if (PHPUnit_MAIN_METHOD == 'Zend_Ldap_Node_AllTests::main') {
+    Zend_Ldap_Node_AllTests::main();
+}

+ 62 - 0
tests/Zend/Ldap/Node/AttributeIterationTest.php

@@ -0,0 +1,62 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Zend_Ldap_TestCase
+ */
+require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'TestCase.php';
+/**
+ * @see Zend_Ldap_Node
+ */
+require_once 'Zend/Ldap/Node.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+class Zend_Ldap_Node_AttributeIterationTest extends Zend_Ldap_TestCase
+{
+    public function testSimpleIteration()
+    {
+        $node=$this->_createTestNode();
+        $i=0;
+        $data=array();
+        foreach ($node->getAttributes() as $k => $v) {
+            $this->assertNotNull($k);
+            $this->assertNotNull($v);
+            $this->assertEquals($node->$k, $v);
+            $data[$k]=$v;
+            $i++;
+        }
+        $this->assertEquals(5, $i);
+        $this->assertEquals($i, count($node));
+        $this->assertEquals(array(
+            'boolean'     => array(true, false),
+            'cn'          => array('name'),
+            'empty'       => array(),
+            'host'        => array('a', 'b', 'c'),
+            'objectclass' => array('account', 'top')), $data);
+    }
+}

+ 123 - 0
tests/Zend/Ldap/Node/ChildrenIterationTest.php

@@ -0,0 +1,123 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Zend_Ldap_OnlineTestCase
+ */
+require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'OnlineTestCase.php';
+/**
+ * @see Zend_Ldap_Node
+ */
+require_once 'Zend/Ldap/Node.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+class Zend_Ldap_Node_ChildrenIterationTest extends Zend_Ldap_OnlineTestCase
+{
+    protected function setUp()
+    {
+        parent::setUp();
+        $this->_prepareLdapServer();
+    }
+
+    protected function tearDown()
+    {
+        $this->_cleanupLdapServer();
+        parent::tearDown();
+    }
+
+    public function testSimpleIteration()
+    {
+        $node=$this->_getLdap()->getBaseNode();
+        $children=$node->getChildren();
+
+        $i=1;
+        foreach ($children as $rdn => $n) {
+            $dn=$n->getDn()->toString(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER);
+            $rdn=Zend_Ldap_Dn::implodeRdn($n->getRdnArray(), Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER);
+            if ($i==1) {
+                $this->assertEquals('ou=Node', $rdn);
+                $this->assertEquals($this->_createDn('ou=Node,'), $dn);
+            }
+            else {
+                $this->assertEquals('ou=Test' . ($i-1), $rdn);
+                $this->assertEquals($this->_createDn('ou=Test' . ($i-1) . ','), $dn);
+            }
+            $i++;
+        }
+        $this->assertEquals(6, $i-1);
+    }
+
+    public function testSimpleRecursiveIteration()
+    {
+        $node=$this->_getLdap()->getBaseNode();
+        $ri=new RecursiveIteratorIterator($node, RecursiveIteratorIterator::SELF_FIRST);
+        $i=0;
+        foreach ($ri as $rdn => $n) {
+            $dn=$n->getDn()->toString(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER);
+            $rdn=Zend_Ldap_Dn::implodeRdn($n->getRdnArray(), Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER);
+            if ($i==0) {
+                $this->assertEquals(Zend_Ldap_Dn::fromString(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE)
+                    ->toString(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER), $dn);
+            }
+            else if ($i==1) {
+                $this->assertEquals('ou=Node', $rdn);
+                $this->assertEquals($this->_createDn('ou=Node,'), $dn);
+            }
+            else {
+                if ($i<4) {
+                    $j=$i-1;
+                    $base=$this->_createDn('ou=Node,');
+                }
+                else {
+                    $j=$i-3;
+                    $base=Zend_Ldap_Dn::fromString(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE)
+                        ->toString(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER);
+                }
+                $this->assertEquals('ou=Test' . $j, $rdn);
+                $this->assertEquals('ou=Test' . $j . ',' . $base, $dn);
+            }
+            $i++;
+        }
+        $this->assertEquals(9, $i);
+    }
+
+    /**
+     * Test issue reported by Lance Hendrix on
+     * http://framework.zend.com/wiki/display/ZFPROP/Zend_Ldap+-+Extended+support+-+Stefan+Gehrig?
+     *      focusedCommentId=13107431#comment-13107431
+     */
+    public function testCallingNextAfterIterationShouldNotThrowException()
+    {
+        $node = $this->_getLdap()->getBaseNode();
+        $nodes = $node->searchChildren('(objectClass=*)');
+        foreach ($nodes as $rdn => $n) {
+            // do nothing - just iterate
+        }
+        $nodes->next();
+    }
+}

+ 205 - 0
tests/Zend/Ldap/Node/ChildrenTest.php

@@ -0,0 +1,205 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Zend_Ldap_OnlineTestCase
+ */
+require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'OnlineTestCase.php';
+/**
+ * @see Zend_Ldap_Node
+ */
+require_once 'Zend/Ldap/Node.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+class Zend_Ldap_Node_ChildrenTest extends Zend_Ldap_OnlineTestCase
+{
+    protected function setUp()
+    {
+        parent::setUp();
+        $this->_prepareLdapServer();
+    }
+
+    protected function tearDown()
+    {
+        $this->_cleanupLdapServer();
+        parent::tearDown();
+    }
+
+    public function testGetChildrenOnAttachedNode()
+    {
+        $node=$this->_getLdap()->getBaseNode();
+        $children=$node->getChildren();
+        $this->assertType('Zend_Ldap_Node_ChildrenIterator', $children);
+        $this->assertEquals(6, count($children));
+        $this->assertType('Zend_Ldap_Node', $children['ou=Node']);
+    }
+
+    public function testGetChildrenOnDetachedNode()
+    {
+        $node=$this->_getLdap()->getBaseNode();
+        $node->detachLdap();
+        $children=$node->getChildren();
+        $this->assertType('Zend_Ldap_Node_ChildrenIterator', $children);
+        $this->assertEquals(0, count($children));
+
+        $node->attachLdap($this->_getLdap());
+        $node->reload();
+        $children=$node->getChildren();
+
+        $this->assertType('Zend_Ldap_Node_ChildrenIterator', $children);
+        $this->assertEquals(6, count($children));
+        $this->assertType('Zend_Ldap_Node', $children['ou=Node']);
+    }
+
+    public function testHasChildrenOnAttachedNode()
+    {
+        $node=$this->_getLdap()->getNode(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE);
+        $this->assertTrue($node->hasChildren());
+        $this->assertTrue($node->hasChildren());
+
+        $node=$this->_getLdap()->getNode($this->_createDn('ou=Node,'));
+        $this->assertTrue($node->hasChildren());
+        $this->assertTrue($node->hasChildren());
+
+        $node=$this->_getLdap()->getNode($this->_createDn('ou=Test1,'));
+        $this->assertFalse($node->hasChildren());
+        $this->assertFalse($node->hasChildren());
+
+        $node=$this->_getLdap()->getNode($this->_createDn('ou=Test1,ou=Node,'));
+        $this->assertFalse($node->hasChildren());
+        $this->assertFalse($node->hasChildren());
+    }
+
+    public function testHasChildrenOnDetachedNodeWithoutPriorGetChildren()
+    {
+        $node=$this->_getLdap()->getNode(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE);
+        $node->detachLdap();
+        $this->assertFalse($node->hasChildren());
+
+        $node=$this->_getLdap()->getNode($this->_createDn('ou=Node,'));
+        $node->detachLdap();
+        $this->assertFalse($node->hasChildren());
+
+        $node=$this->_getLdap()->getNode($this->_createDn('ou=Test1,'));
+        $node->detachLdap();
+        $this->assertFalse($node->hasChildren());
+
+        $node=$this->_getLdap()->getNode($this->_createDn('ou=Test1,ou=Node,'));
+        $node->detachLdap();
+        $this->assertFalse($node->hasChildren());
+    }
+
+    public function testHasChildrenOnDetachedNodeWithPriorGetChildren()
+    {
+        $node=$this->_getLdap()->getNode(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE);
+        $node->getChildren();
+        $node->detachLdap();
+        $this->assertTrue($node->hasChildren());
+
+        $node=$this->_getLdap()->getNode($this->_createDn('ou=Node,'));
+        $node->getChildren();
+        $node->detachLdap();
+        $this->assertTrue($node->hasChildren());
+
+        $node=$this->_getLdap()->getNode($this->_createDn('ou=Test1,'));
+        $node->getChildren();
+        $node->detachLdap();
+        $this->assertFalse($node->hasChildren());
+
+        $node=$this->_getLdap()->getNode($this->_createDn('ou=Test1,ou=Node,'));
+        $node->getChildren();
+        $node->detachLdap();
+        $this->assertFalse($node->hasChildren());
+    }
+
+    public function testChildrenCollectionSerialization()
+    {
+        $node=$this->_getLdap()->getNode($this->_createDn('ou=Node,'));
+
+        $children=$node->getChildren();
+        $this->assertTrue($node->hasChildren());
+        $this->assertEquals(2, count($children));
+
+        $string=serialize($node);
+        $node2=unserialize($string);
+
+        $children2=$node2->getChildren();
+        $this->assertTrue($node2->hasChildren());
+        $this->assertEquals(2, count($children2));
+
+        $node2->attachLdap($this->_getLdap());
+
+        $children2=$node2->getChildren();
+        $this->assertTrue($node2->hasChildren());
+        $this->assertEquals(2, count($children2));
+
+        $node=$this->_getLdap()->getNode($this->_createDn('ou=Node,'));
+        $this->assertTrue($node->hasChildren());
+        $string=serialize($node);
+        $node2=unserialize($string);
+        $this->assertFalse($node2->hasChildren());
+        $node2->attachLdap($this->_getLdap());
+        $this->assertTrue($node2->hasChildren());
+    }
+
+    public function testCascadingAttachAndDetach()
+    {
+        $node=$this->_getLdap()->getBaseNode();
+        $baseChildren=$node->getChildren();
+        $nodeChildren=$baseChildren['ou=Node']->getChildren();
+
+        $this->assertTrue($node->isAttached());
+        foreach ($baseChildren as $bc) {
+            $this->assertTrue($bc->isAttached());
+        }
+        foreach ($nodeChildren as $nc) {
+            $this->assertTrue($nc->isAttached());
+        }
+
+        $node->detachLdap();
+        $this->assertFalse($node->isAttached());
+        foreach ($baseChildren as $bc) {
+            $this->assertFalse($bc->isAttached());
+        }
+        foreach ($nodeChildren as $nc) {
+            $this->assertFalse($nc->isAttached());
+        }
+
+        $node->attachLdap($this->_getLdap());
+        $this->assertTrue($node->isAttached());
+        $this->assertSame($this->_getLdap(), $node->getLdap());
+        foreach ($baseChildren as $bc) {
+            $this->assertTrue($bc->isAttached());
+            $this->assertSame($this->_getLdap(), $bc->getLdap());
+        }
+        foreach ($nodeChildren as $nc) {
+            $this->assertTrue($nc->isAttached());
+            $this->assertSame($this->_getLdap(), $nc->getLdap());
+        }
+    }
+}

+ 596 - 0
tests/Zend/Ldap/Node/OfflineTest.php

@@ -0,0 +1,596 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Zend_Ldap_TestCase
+ */
+require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'TestCase.php';
+/**
+ * Zend_Ldap_Node
+ */
+require_once 'Zend/Ldap/Node.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+class Zend_Ldap_Node_OfflineTest extends Zend_Ldap_TestCase
+{
+    protected function _assertLocalDateTimeString($timestamp, $value)
+    {
+        $this->assertEquals(date('YmdHisO', $timestamp), $value);
+    }
+
+    protected function _assertUtcDateTimeString($localTimestamp, $value)
+    {
+        $currentOffset = date('Z');
+        $utcTimestamp = $localTimestamp - $currentOffset;
+        $this->assertEquals(date('YmdHis', $utcTimestamp) . 'Z', $value);
+    }
+
+    public function testCreateFromArrayStringDn()
+    {
+        $data=$this->_createTestArrayData();
+        $node=Zend_Ldap_Node::fromArray($data);
+        $this->assertType('Zend_Ldap_Node', $node);
+        $this->assertFalse($node->isAttached());
+        $this->assertFalse($node->willBeDeleted());
+        $this->assertFalse($node->willBeMoved());
+        $this->assertTrue($node->isNew());
+    }
+
+    public function testCreateFromArrayObjectDn()
+    {
+        $data=$this->_createTestArrayData();
+        $data['dn']=Zend_Ldap_Dn::fromString($data['dn']);
+        $node=Zend_Ldap_Node::fromArray($data);
+        $this->assertType('Zend_Ldap_Node', $node);
+        $this->assertFalse($node->isAttached());
+    }
+
+    /**
+     * @expectedException Zend_Ldap_Exception
+     */
+    public function testCreateFromArrayMissingDn()
+    {
+        $data=$this->_createTestArrayData();
+        unset($data['dn']);
+        $node=Zend_Ldap_Node::fromArray($data);
+    }
+
+    /**
+     * @expectedException Zend_Ldap_Exception
+     */
+    public function testCreateFromArrayIllegalDn()
+    {
+        $data=$this->_createTestArrayData();
+        $data['dn']=5;
+        $node=Zend_Ldap_Node::fromArray($data);
+    }
+
+    /**
+     * @expectedException Zend_Ldap_Exception
+     */
+    public function testCreateFromArrayMalformedDn()
+    {
+        $data=$this->_createTestArrayData();
+        $data['dn']='name1,cn=name2,dc=example,dc=org';
+        $node=Zend_Ldap_Node::fromArray($data);
+    }
+
+    public function testCreateFromArrayAndEnsureRdnValues()
+    {
+        $data=$this->_createTestArrayData();
+        $data['dn']=Zend_Ldap_Dn::fromString($data['dn']);
+        $node=Zend_Ldap_Node::fromArray($data);
+        $this->assertType('Zend_Ldap_Node', $node);
+        $this->assertFalse($node->isAttached());
+        unset($data['dn']);
+        $this->assertEquals($data, $node->getData());
+    }
+
+    public function testGetDnString()
+    {
+        $data=$this->_createTestArrayData();
+        $node=Zend_Ldap_Node::fromArray($data);
+        $this->assertEquals($data['dn'], $node->getDnString());
+    }
+
+    public function testGetDnArray()
+    {
+        $data=$this->_createTestArrayData();
+        $node=Zend_Ldap_Node::fromArray($data);
+        $exA=Zend_Ldap_Dn::explodeDn($data['dn']);
+        $this->assertEquals($exA, $node->getDnArray());
+    }
+
+    public function testGetDnObject()
+    {
+        $data=$this->_createTestArrayData();
+        $node=Zend_Ldap_Node::fromArray($data);
+        $compareDn=Zend_Ldap_Dn::fromString('cn=name,dc=example,dc=org');
+        $this->assertEquals($compareDn, $node->getDn());
+        $this->assertNotSame($node->getDn(), $node->getDn());
+    }
+
+    public function testGetRdnString()
+    {
+        $node=$this->_createTestNode();
+        $this->assertEquals('cn=name', $node->getRdnString());
+    }
+
+    public function testGetRdnArray()
+    {
+        $node=$this->_createTestNode();
+        $this->assertEquals(array('cn' => 'name'), $node->getRdnArray());
+    }
+
+    public function testSerialize()
+    {
+        $node=$this->_createTestNode();
+        $sdata=serialize($node);
+        $newObject=unserialize($sdata);
+        $this->assertEquals($node, $newObject);
+    }
+
+    public function testToString()
+    {
+        $node=$this->_createTestNode();
+        $this->assertEquals('cn=name,dc=example,dc=org', $node->toString());
+        $this->assertEquals('cn=name,dc=example,dc=org', (string)$node);
+    }
+
+    public function testToArray()
+    {
+        $node=$this->_createTestNode();
+        $this->assertEquals(array(
+            'dn'          => 'cn=name,dc=example,dc=org',
+            'cn'          => array('name'),
+            'host'        => array('a', 'b', 'c'),
+            'empty'       => array(),
+            'boolean'     => array(true, false),
+            'objectclass' => array('account', 'top'),
+        ), $node->toArray());
+    }
+
+    public function testToJson()
+    {
+        $node=$this->_createTestNode();
+        $this->assertEquals('{"dn":"cn=name,dc=example,dc=org",' .
+            '"boolean":[true,false],' .
+            '"cn":["name"],' .
+            '"empty":[],' .
+            '"host":["a","b","c"],' .
+            '"objectclass":["account","top"]}', $node->toJson());
+    }
+
+    public function testGetData()
+    {
+        $data=$this->_createTestArrayData();
+        $node=Zend_Ldap_Node::fromArray($data);
+        ksort($data, SORT_STRING);
+        unset($data['dn']);
+        $this->assertEquals($data, $node->getData());
+    }
+
+    public function testGetObjectClass()
+    {
+        $node=$this->_createTestNode();
+        $this->assertEquals(array('account', 'top'), $node->getObjectClass());
+    }
+
+    public function testModifyObjectClass()
+    {
+        $node=$this->_createTestNode();
+        $this->assertEquals(array('account', 'top'), $node->getObjectClass());
+
+        $node->setObjectClass('domain');
+        $this->assertEquals(array('domain'), $node->getObjectClass());
+
+        $node->setObjectClass(array('account', 'top'));
+        $this->assertEquals(array('account', 'top'), $node->getObjectClass());
+
+        $node->appendObjectClass('domain');
+        $this->assertEquals(array('account', 'top', 'domain'), $node->getObjectClass());
+
+        $node->setObjectClass('domain');
+        $node->appendObjectClass(array('account', 'top'));
+        $this->assertEquals(array('domain', 'account', 'top'), $node->getObjectClass());
+    }
+
+    public function testGetAttributes()
+    {
+        $node=$this->_createTestNode();
+        $expected=array(
+            'boolean'     => array(true, false),
+            'cn'          => array('name'),
+            'empty'       => array(),
+            'host'        => array('a', 'b', 'c'),
+            'objectclass' => array('account', 'top'),
+        );
+        $this->assertEquals($expected, $node->getAttributes());
+        $this->assertFalse($node->willBeDeleted());
+        $this->assertFalse($node->willBeMoved());
+        $this->assertFalse($node->isNew());
+
+        $node->delete();
+        $this->assertTrue($node->willBeDeleted());
+    }
+
+    public function testAppendToAttributeFirstTime()
+    {
+        $node=$this->_createTestNode();
+        $node->appendToAttribute('host', 'newHost');
+        $ts=mktime(12, 30, 30, 6, 25, 2008);
+        $node->appendToDateTimeAttribute('objectClass', $ts);
+        $this->assertEquals('newHost', $node->host[3]);
+        $this->assertEquals($ts, $node->getDateTimeAttribute('objectClass', 2));
+    }
+
+    public function testExistsAttribute()
+    {
+        $node=$this->_createTestNode();
+        $this->assertFalse($node->existsAttribute('nonExistant'));
+        $this->assertFalse($node->existsAttribute('empty', false));
+        $this->assertTrue($node->existsAttribute('empty', true));
+
+        $node->newEmpty=null;
+        $this->assertFalse($node->existsAttribute('newEmpty', false));
+        $this->assertTrue($node->existsAttribute('newEmpty', true));
+
+        $node->empty='string';
+        $this->assertTrue($node->existsAttribute('empty', false));
+        $this->assertTrue($node->existsAttribute('empty', true));
+
+        $node->deleteAttribute('empty');
+        $this->assertFalse($node->existsAttribute('empty', false));
+        $this->assertTrue($node->existsAttribute('empty', true));
+    }
+
+    public function testGetSetAndDeleteMethods()
+    {
+        $node=$this->_createTestNode();
+
+        $node->setAttribute('key', 'value1');
+        $this->assertEquals('value1', $node->getAttribute('key', 0));
+        $node->appendToAttribute('key', 'value2');
+        $this->assertEquals('value1', $node->getAttribute('key', 0));
+        $this->assertEquals('value2', $node->getAttribute('key', 1));
+        $this->assertTrue($node->existsAttribute('key', true));
+        $this->assertTrue($node->existsAttribute('key', false));
+        $node->deleteAttribute('key');
+        $this->assertEquals(0, count($node->getAttribute('key')));
+        $this->assertTrue($node->existsAttribute('key', true));
+        $this->assertFalse($node->existsAttribute('key', false));
+
+        $ts=mktime(12, 30, 30, 6, 25, 2008);
+        $node->setDateTimeAttribute('key', $ts, false);
+        $this->_assertLocalDateTimeString($ts, $node->getAttribute('key', 0));
+        $this->assertEquals($ts, $node->getDateTimeAttribute('key', 0));
+        $node->appendToDateTimeAttribute('key', $ts, true);
+        $this->_assertLocalDateTimeString($ts, $node->getAttribute('key', 0));
+        $this->assertEquals($ts, $node->getDateTimeAttribute('key', 0));
+        $this->_assertUtcDateTimeString($ts, $node->getAttribute('key', 1));
+        $this->assertEquals($ts, $node->getDateTimeAttribute('key', 1));
+        $this->assertTrue($node->existsAttribute('key', true));
+        $this->assertTrue($node->existsAttribute('key', false));
+        $node->deleteAttribute('key');
+        $this->assertEquals(0, count($node->getAttribute('key')));
+        $this->assertTrue($node->existsAttribute('key', true));
+        $this->assertFalse($node->existsAttribute('key', false));
+
+        $node->setPasswordAttribute('pa$$w0rd', Zend_Ldap_Attribute::PASSWORD_HASH_MD5);
+        $this->assertEquals('{MD5}bJuLJ96h3bhF+WqiVnxnVA==', $node->getAttribute('userPassword', 0));
+        $this->assertTrue($node->existsAttribute('userPassword', true));
+        $this->assertTrue($node->existsAttribute('userPassword', false));
+        $node->deleteAttribute('userPassword');
+        $this->assertEquals(0, count($node->getAttribute('userPassword')));
+        $this->assertTrue($node->existsAttribute('userPassword', true));
+        $this->assertFalse($node->existsAttribute('userPassword', false));
+    }
+
+    public function testOverloading()
+    {
+        $node=$this->_createTestNode();
+
+        $node->key='value1';
+        $this->assertEquals('value1', $node->key[0]);
+        $this->assertTrue(isset($node->key));
+        unset($node->key);
+        $this->assertEquals(0, count($node->key));
+        $this->assertFalse(isset($node->key));
+    }
+
+    /**
+     * @expectedException Zend_Ldap_Exception
+     */
+    public function testIllegalAttributeAccessRdnAttributeSet()
+    {
+        $node=$this->_createTestNode();
+        $node->cn='test';
+    }
+
+    /**
+     * @expectedException Zend_Ldap_Exception
+     */
+    public function testIllegalAttributeAccessDnSet()
+    {
+        $node=$this->_createTestNode();
+        $node->dn='test';
+    }
+
+    public function testAttributeAccessDnGet()
+    {
+        $node=$this->_createTestNode();
+        $this->assertType('string', $node->dn);
+        $this->assertEquals($node->getDn()->toString(), $node->dn);
+    }
+
+    public function testArrayAccess()
+    {
+        $node=$this->_createTestNode();
+
+        $node['key']='value1';
+        $this->assertEquals('value1', $node['key'][0]);
+        $this->assertTrue(isset($node['key']));
+        unset($node['key']);
+        $this->assertEquals(0, count($node['key']));
+        $this->assertFalse(isset($node['key']));
+    }
+
+    public function testCreateEmptyNode()
+    {
+        $dn='cn=name,dc=example,dc=org';
+        $objectClass=array('account', 'test', 'inetOrgPerson');
+        $node=Zend_Ldap_Node::create($dn, $objectClass);
+        $this->assertEquals($dn, $node->getDnString());
+        $this->assertEquals('cn=name', $node->getRdnString());
+        $this->assertEquals('name', $node->cn[0]);
+        $this->assertEquals($objectClass, $node->objectClass);
+        $this->assertFalse($node->willBeDeleted());
+        $this->assertFalse($node->willBeMoved());
+        $this->assertTrue($node->isNew());
+
+        $node->delete();
+        $this->assertTrue($node->willBeDeleted());
+    }
+
+    public function testGetChangedData()
+    {
+        $node=$this->_createTestNode();
+        $node->host=array('d');
+        $node->empty='not Empty';
+        unset($node->objectClass);
+        $changedData=$node->getChangedData();
+        $this->assertEquals(array('d'), $changedData['host']);
+        $this->assertEquals(array('not Empty'), $changedData['empty']);
+        $this->assertEquals(array(), $changedData['objectclass']);
+    }
+
+    public function testDeleteUnusedAttribute()
+    {
+        $node=$this->_createTestNode();
+        $node->deleteAttribute('nonexistant');
+        $changedData=$node->getChangedData();
+        $this->assertArrayNotHasKey('nonexistant', $changedData);
+    }
+
+    public function testRenameNodeString()
+    {
+        $data=$this->_createTestArrayData();
+        $node=Zend_Ldap_Node::fromArray($data);
+
+        $newDnString='cn=test+ou=Lab+uid=tester,cn=name,dc=example,dc=org';
+        $node->setDn($newDnString);
+        $this->assertEquals($data['dn'], $node->getCurrentDn()->toString());
+        $this->assertEquals($newDnString, $node->getDn()->toString());
+        $this->assertEquals(array('test'), $node->cn);
+        $this->assertEquals(array('tester'), $node->uid);
+        $this->assertEquals(array('Lab'), $node->ou);
+
+        $this->assertFalse($node->willBeDeleted());
+        $this->assertFalse($node->willBeMoved());
+        $this->assertTrue($node->isNew());
+    }
+
+    public function testRenameNodeArray()
+    {
+        $data=$this->_createTestArrayData();
+        $node=Zend_Ldap_Node::fromArray($data);
+
+        $newDnArray=array(
+            array('uid' => 'tester'),
+            array('dc' => 'example'),
+            array('dc' => 'org'));
+
+        $node->setDn($newDnArray);
+        $this->assertEquals($data['dn'], $node->getCurrentDn()->toString());
+        $this->assertEquals($newDnArray, $node->getDn()->toArray());
+        $this->assertEquals(array('name'), $node->cn);
+    }
+
+    public function testRenameNodeDnObject()
+    {
+        $data=$this->_createTestArrayData();
+        $node=Zend_Ldap_Node::fromArray($data);
+
+        $newDn=Zend_Ldap_Dn::fromString('cn=test+ou=Lab+uid=tester,cn=name,dc=example,dc=org');
+        $node->setDn($newDn);
+        $this->assertEquals($data['dn'], $node->getCurrentDn()->toString());
+        $this->assertEquals($newDn, $node->getDn());
+        $this->assertEquals(array('test'), $node->cn);
+        $this->assertEquals(array('tester'), $node->uid);
+        $this->assertEquals(array('Lab'), $node->ou);
+    }
+
+    public function testRenameNodeFromDataSource()
+    {
+        $node=$this->_createTestNode();
+        $newDnString='cn=test+ou=Lab+uid=tester,cn=name,dc=example,dc=org';
+        $node->rename($newDnString);
+
+        $this->assertFalse($node->willBeDeleted());
+        $this->assertTrue($node->willBeMoved());
+        $this->assertFalse($node->isNew());
+    }
+
+    public function testDnObjectCloning()
+    {
+        $node1=$this->_createTestNode();
+        $dn1=Zend_Ldap_Dn::fromString('cn=name2,dc=example,dc=org');
+        $node1->setDn($dn1);
+        $dn1->prepend(array('cn' => 'name'));
+        $this->assertNotEquals($dn1->toString(), $node1->getDn()->toString());
+
+        $dn2=Zend_Ldap_Dn::fromString('cn=name2,dc=example,dc=org');
+        $node2=Zend_Ldap_Node::create($dn2);
+        $dn2->prepend(array('cn' => 'name'));
+        $this->assertNotEquals($dn2->toString(), $node2->getDn()->toString());
+
+        $dn3=Zend_Ldap_Dn::fromString('cn=name2,dc=example,dc=org');
+        $node3=Zend_Ldap_Node::fromArray(array(
+            'dn' => $dn3,
+            'ou' => 'Test'), false);
+        $dn3->prepend(array('cn' => 'name'));
+        $this->assertNotEquals($dn3->toString(), $node3->getDn()->toString());
+    }
+
+    public function testGetChanges()
+    {
+        $node=$this->_createTestNode();
+        $node->host=array('d');
+        $node->empty='not Empty';
+        unset($node->boolean);
+        $changes=$node->getChanges();
+        $this->assertEquals(array(
+            'add'     => array(
+                'empty' => array('not Empty')
+            ),
+            'delete'  => array(
+                'boolean' => array()
+            ),
+            'replace' => array(
+                'host' => array('d')
+            )
+        ), $changes);
+
+        $node=Zend_Ldap_Node::create('uid=test,dc=example,dc=org', array('account'));
+        $node->host='host';
+        unset($node->cn);
+        unset($node['sn']);
+        $node['givenName']='givenName';
+        $node->appendToAttribute('objectClass', 'domain');
+        $this->assertEquals(array(
+            'uid' => array('test'),
+            'objectclass' => array('account', 'domain'),
+            'host' => array('host'),
+            'givenname' => array('givenName')
+        ), $node->getChangedData());
+        $this->assertEquals(array(
+            'add'     => array(
+                'uid' => array('test'),
+                'objectclass' => array('account', 'domain'),
+                'host' => array('host'),
+                'givenname' => array('givenName'),
+            ),
+            'delete'  => array(),
+            'replace' => array()
+        ), $node->getChanges());
+    }
+
+    public function testHasValue()
+    {
+        $node=$this->_createTestNode();
+
+        $this->assertTrue($node->attributeHasValue('cn', 'name'));
+        $this->assertFalse($node->attributeHasValue('cn', 'noname'));
+        $this->assertTrue($node->attributeHasValue('boolean', true));
+        $this->assertTrue($node->attributeHasValue('boolean', false));
+
+        $this->assertTrue($node->attributeHasValue('host', array('a', 'b')));
+        $this->assertTrue($node->attributeHasValue('host', array('a', 'b', 'c')));
+        $this->assertFalse($node->attributeHasValue('host', array('a', 'b', 'c', 'd')));
+        $this->assertTrue($node->attributeHasValue('boolean', array(true, false)));
+    }
+
+    public function testRemoveDuplicates()
+    {
+        $node=$this->_createTestNode();
+        $node->strings1= array('value1', 'value2', 'value2', 'value3');
+        $node->strings2= array('value1', 'value2', 'value3', 'value4');
+        $node->boolean1= array(true, true, true, true);
+        $node->boolean2= array(true, false, true, false);
+
+        $expected=array(
+            'cn'          => array('name'),
+            'host'        => array('a', 'b', 'c'),
+            'empty'       => array(),
+            'boolean'     => array('TRUE', 'FALSE'),
+            'objectclass' => array('account', 'top'),
+            'strings1' => array('value1', 'value2', 'value3'),
+            'strings2' => array('value1', 'value2', 'value3', 'value4'),
+            'boolean1' => array('TRUE'),
+            'boolean2' => array('TRUE', 'FALSE'),
+        );
+
+        $node->removeDuplicatesFromAttribute('strings1');
+        $node->removeDuplicatesFromAttribute('strings2');
+        $node->removeDuplicatesFromAttribute('boolean1');
+        $node->removeDuplicatesFromAttribute('boolean2');
+        $this->assertEquals($expected, $node->getData(false));
+    }
+
+    public function testRemoveFromAttributeSimple()
+    {
+        $node=$this->_createTestNode();
+        $node->test=array('value1', 'value2', 'value3', 'value3');
+        $node->removeFromAttribute('test', 'value2');
+        $this->assertEquals(array('value1', 'value3', 'value3'), $node->test);
+    }
+
+    public function testRemoveFromAttributeArray()
+    {
+        $node=$this->_createTestNode();
+        $node->test=array('value1', 'value2', 'value3', 'value3');
+        $node->removeFromAttribute('test', array('value1', 'value2'));
+        $this->assertEquals(array('value3', 'value3'), $node->test);
+    }
+
+    public function testRemoveFromAttributeMultipleSimple()
+    {
+        $node=$this->_createTestNode();
+        $node->test=array('value1', 'value2', 'value3', 'value3');
+        $node->removeFromAttribute('test', 'value3');
+        $this->assertEquals(array('value1', 'value2'), $node->test);
+    }
+
+    public function testRemoveFromAttributeMultipleArray()
+    {
+        $node=$this->_createTestNode();
+        $node->test=array('value1', 'value2', 'value3', 'value3');
+        $node->removeFromAttribute('test', array('value1', 'value3'));
+        $this->assertEquals(array('value2'), $node->test);
+    }
+}

+ 287 - 0
tests/Zend/Ldap/Node/OnlineTest.php

@@ -0,0 +1,287 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Zend_Ldap_OnlineTestCase
+ */
+require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'OnlineTestCase.php';
+/**
+ * @see Zend_Ldap_Node
+ */
+require_once 'Zend/Ldap/Node.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+class Zend_Ldap_Node_OnlineTest extends Zend_Ldap_OnlineTestCase
+{
+    protected function setUp()
+    {
+        parent::setUp();
+        $this->_prepareLdapServer();
+    }
+
+    protected function tearDown()
+    {
+        $this->_cleanupLdapServer();
+        parent::tearDown();
+    }
+
+    public function testLoadFromLdap()
+    {
+        $dn=$this->_createDn('ou=Test1,');
+        $node=Zend_Ldap_Node::fromLdap($dn, $this->_getLdap());
+        $this->assertType('Zend_Ldap_Node', $node);
+        $this->assertTrue($node->isAttached());
+    }
+
+    public function testChangeReadOnlySystemAttributes()
+    {
+        $node=$this->_getLdap()->getBaseNode();
+        try {
+            $node->setAttribute('createTimestamp', false);
+            $this->fail('Expected exception for modification of read-only attribute createTimestamp');
+        } catch (Zend_Ldap_Exception $e) {
+            $this->assertEquals('Cannot change attribute because it\'s read-only', $e->getMessage());
+        }
+        try {
+            $node->createTimestamp=false;
+            $this->fail('Expected exception for modification of read-only attribute createTimestamp');
+        } catch (Zend_Ldap_Exception $e) {
+            $this->assertEquals('Cannot change attribute because it\'s read-only', $e->getMessage());
+        }
+        try {
+            $node['createTimestamp']=false;
+            $this->fail('Expected exception for modification of read-only attribute createTimestamp');
+        } catch (Zend_Ldap_Exception $e) {
+            $this->assertEquals('Cannot change attribute because it\'s read-only', $e->getMessage());
+        }
+        try {
+            $node->appendToAttribute('createTimestamp', 'value');
+            $this->fail('Expected exception for modification of read-only attribute createTimestamp');
+        } catch (Zend_Ldap_Exception $e) {
+            $this->assertEquals('Cannot change attribute because it\'s read-only', $e->getMessage());
+        }
+        try {
+            $rdn=$node->getRdnArray(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER);
+            $attr=key($rdn);
+            $node->deleteAttribute($attr);
+            $this->fail('Expected exception for modification of read-only attribute ' . $attr);
+        } catch (Zend_Ldap_Exception $e) {
+            $this->assertEquals('Cannot change attribute because it\'s part of the RDN', $e->getMessage());
+        }
+    }
+
+    /**
+     * @expectedException Zend_Ldap_Exception
+     */
+    public function testLoadFromLdapIllegalEntry()
+    {
+        $dn=$this->_createDn('ou=Test99,');
+        $node=Zend_Ldap_Node::fromLdap($dn, $this->_getLdap());
+    }
+
+    public function testDetachAndReattach()
+    {
+        $dn=$this->_createDn('ou=Test1,');
+        $node=Zend_Ldap_Node::fromLdap($dn, $this->_getLdap());
+        $this->assertType('Zend_Ldap_Node', $node);
+        $this->assertTrue($node->isAttached());
+        $node->detachLdap();
+        $this->assertFalse($node->isAttached());
+        $node->attachLdap($this->_getLdap());
+        $this->assertTrue($node->isAttached());
+    }
+
+    public function testSerialize()
+    {
+        $dn=$this->_createDn('ou=Test1,');
+        $node=Zend_Ldap_Node::fromLdap($dn, $this->_getLdap());
+        $sdata=serialize($node);
+        $newObject=unserialize($sdata);
+        $this->assertFalse($newObject->isAttached());
+        $this->assertTrue($node->isAttached());
+        $this->assertEquals($sdata, serialize($newObject));
+    }
+
+    /**
+     * @expectedException Zend_Ldap_Exception
+     */
+    public function testAttachToInvalidLdap()
+    {
+        $data=array(
+            'dn'          => 'ou=name,dc=example,dc=org',
+            'ou'          => array('name'),
+            'l'           => array('a', 'b', 'c'),
+            'objectClass' => array('organizationalUnit', 'top'),
+        );
+        $node=Zend_Ldap_Node::fromArray($data);
+        $this->assertFalse($node->isAttached());
+        $node->attachLdap($this->_getLdap());
+    }
+
+    public function testAttachToValidLdap()
+    {
+        $data=array(
+            'dn'          => $this->_createDn('ou=name,'),
+            'ou'          => array('name'),
+            'l'           => array('a', 'b', 'c'),
+            'objectClass' => array('organizationalUnit', 'top'),
+        );
+        $node=Zend_Ldap_Node::fromArray($data);
+        $this->assertFalse($node->isAttached());
+        $node->attachLdap($this->_getLdap());
+        $this->assertTrue($node->isAttached());
+    }
+
+    public function testExistsDn()
+    {
+        $data=array(
+            'dn'          => $this->_createDn('ou=name,'),
+            'ou'          => array('name'),
+            'l'           => array('a', 'b', 'c'),
+            'objectClass' => array('organizationalUnit', 'top'),
+        );
+        $node1=Zend_Ldap_Node::fromArray($data);
+        $node1->attachLdap($this->_getLdap());
+        $this->assertFalse($node1->exists());
+        $dn=$this->_createDn('ou=Test1,');
+        $node2=Zend_Ldap_Node::fromLdap($dn, $this->_getLdap());
+        $this->assertTrue($node2->exists());
+    }
+
+    public function testReload()
+    {
+        $dn=$this->_createDn('ou=Test1,');
+        $node=Zend_Ldap_Node::fromLdap($dn, $this->_getLdap());
+        $node->reload();
+        $this->assertEquals($dn, $node->getDn()->toString());
+        $this->assertEquals('ou=Test1', $node->getRdnString());
+    }
+
+    public function testGetNode()
+    {
+        $dn=$this->_createDn('ou=Test1,');
+        $node=$this->_getLdap()->getNode($dn);
+        $this->assertEquals($dn, $node->getDn()->toString());
+        $this->assertEquals("Test1", $node->getAttribute('ou', 0));
+    }
+
+    /**
+     * @expectedException Zend_Ldap_Exception
+     */
+    public function testGetIllegalNode()
+    {
+        $dn=$this->_createDn('ou=Test99,');
+        $node=$this->_getLdap()->getNode($dn);
+    }
+
+    public function testGetBaseNode()
+    {
+        $node=$this->_getLdap()->getBaseNode();
+        $this->assertEquals(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, $node->getDnString());
+
+        $dn=Zend_Ldap_Dn::fromString(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE,
+            Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER);
+        $this->assertEquals($dn[0]['ou'], $node->getAttribute('ou', 0));
+    }
+
+    public function testSearchSubtree()
+    {
+        $node=$this->_getLdap()->getNode($this->_createDn('ou=Node,'));
+        $items=$node->searchSubtree('(objectClass=organizationalUnit)', Zend_Ldap::SEARCH_SCOPE_SUB,
+            array(), 'ou');
+        $this->assertType('Zend_Ldap_Node_Collection', $items);
+        $this->assertEquals(3, $items->count());
+
+        $i=0;
+        $dns=array(
+            $this->_createDn('ou=Node,'),
+            $this->_createDn('ou=Test1,ou=Node,'),
+            $this->_createDn('ou=Test2,ou=Node,'));
+        foreach ($items as $key => $node) {
+            $key=Zend_Ldap_Dn::fromString($key)->toString(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER);
+            $this->assertEquals($dns[$i], $key);
+            if ($i === 0) {
+                $this->assertEquals('Node', $node->ou[0]);
+            } else {
+                $this->assertEquals('Test' . $i, $node->ou[0]);
+            }
+            $this->assertEquals($key, $node->getDnString(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER));
+            $i++;
+        }
+        $this->assertEquals(3, $i);
+    }
+
+    public function testCountSubtree()
+    {
+        $node=$this->_getLdap()->getNode(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE);
+        $this->assertEquals(9, $node->countSubtree('(objectClass=organizationalUnit)',
+            Zend_Ldap::SEARCH_SCOPE_SUB));
+    }
+
+    public function testCountChildren()
+    {
+        $node=$this->_getLdap()->getNode(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE);
+        $this->assertEquals(6, $node->countChildren());
+        $node=$this->_getLdap()->getNode($this->_createDn('ou=Node,'));
+        $this->assertEquals(2, $node->countChildren());
+    }
+
+    public function testSearchChildren()
+    {
+        $node=$this->_getLdap()->getNode($this->_createDn('ou=Node,'));
+        $this->assertEquals(2, $node->searchChildren('(objectClass=*)', array(), 'ou')->count());
+        $node=$this->_getLdap()->getNode(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE);
+        $this->assertEquals(6, $node->searchChildren('(objectClass=*)', array(), 'ou')->count());
+    }
+
+    public function testGetParent()
+    {
+        $node=$this->_getLdap()->getNode($this->_createDn('ou=Node,'));
+        $pnode=$node->getParent();
+        $this->assertEquals(Zend_Ldap_Dn::fromString(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE)
+            ->toString(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER),
+            $pnode->getDnString(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER));
+    }
+
+    /**
+     * @expectedException Zend_Ldap_Exception
+     */
+    public function testGetNonexistantParent()
+    {
+        $node=$this->_getLdap()->getNode(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE);
+        $pnode=$node->getParent();
+    }
+
+    public function testLoadFromLdapWithDnObject()
+    {
+        $dn=Zend_Ldap_Dn::fromString($this->_createDn('ou=Test1,'));
+        $node=Zend_Ldap_Node::fromLdap($dn, $this->_getLdap());
+        $this->assertType('Zend_Ldap_Node', $node);
+        $this->assertTrue($node->isAttached());
+    }
+}

+ 175 - 0
tests/Zend/Ldap/Node/RootDseTest.php

@@ -0,0 +1,175 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Zend_Ldap_OnlineTestCase
+ */
+require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'OnlineTestCase.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+class Zend_Ldap_Node_RootDseTest extends Zend_Ldap_OnlineTestCase
+{
+    public function testLoadRootDseNode()
+    {
+        $root1=$this->_getLdap()->getRootDse();
+        $root2=$this->_getLdap()->getRootDse();
+
+        $this->assertEquals($root1, $root2);
+        $this->assertSame($root1, $root2);
+    }
+
+    public function testSupportCheckMethods()
+    {
+        $root=$this->_getLdap()->getRootDse();
+
+        $this->assertType('boolean', $root->supportsSaslMechanism('GSSAPI'));
+        $this->assertType('boolean', $root->supportsSaslMechanism(array('GSSAPI', 'DIGEST-MD5')));
+        $this->assertType('boolean', $root->supportsVersion('3'));
+        $this->assertType('boolean', $root->supportsVersion(3));
+        $this->assertType('boolean', $root->supportsVersion(array('3', '2')));
+        $this->assertType('boolean', $root->supportsVersion(array(3, 2)));
+
+        switch ($root->getServerType()) {
+            case Zend_Ldap_Node_RootDse::SERVER_TYPE_ACTIVEDIRECTORY:
+                $this->assertType('boolean', $root->supportsControl('1.2.840.113556.1.4.319'));
+                $this->assertType('boolean', $root->supportsControl(array('1.2.840.113556.1.4.319',
+                    '1.2.840.113556.1.4.473')));
+                $this->assertType('boolean', $root->supportsCapability('1.3.6.1.4.1.4203.1.9.1.1'));
+                $this->assertType('boolean', $root->supportsCapability(array('1.3.6.1.4.1.4203.1.9.1.1',
+                    '2.16.840.1.113730.3.4.18')));
+                $this->assertType('boolean', $root->supportsPolicy('unknown'));
+                $this->assertType('boolean', $root->supportsPolicy(array('unknown', 'unknown')));
+                break;
+            case Zend_Ldap_Node_RootDse::SERVER_TYPE_EDIRECTORY:
+                $this->assertType('boolean', $root->supportsExtension('1.3.6.1.4.1.1466.20037'));
+                $this->assertType('boolean', $root->supportsExtension(array('1.3.6.1.4.1.1466.20037',
+                    '1.3.6.1.4.1.4203.1.11.1')));
+                break;
+            case Zend_Ldap_Node_RootDse::SERVER_TYPE_OPENLDAP:
+                $this->assertType('boolean', $root->supportsControl('1.3.6.1.4.1.4203.1.9.1.1'));
+                $this->assertType('boolean', $root->supportsControl(array('1.3.6.1.4.1.4203.1.9.1.1',
+                    '2.16.840.1.113730.3.4.18')));
+                $this->assertType('boolean', $root->supportsExtension('1.3.6.1.4.1.1466.20037'));
+                $this->assertType('boolean', $root->supportsExtension(array('1.3.6.1.4.1.1466.20037',
+                    '1.3.6.1.4.1.4203.1.11.1')));
+                $this->assertType('boolean', $root->supportsFeature('1.3.6.1.1.14'));
+                $this->assertType('boolean', $root->supportsFeature(array('1.3.6.1.1.14',
+                    '1.3.6.1.4.1.4203.1.5.1')));
+                break;
+        }
+    }
+
+    public function testGetters()
+    {
+        $root=$this->_getLdap()->getRootDse();
+
+        $this->assertType('array', $root->getNamingContexts());
+        $this->assertType('string', $root->getSubschemaSubentry());
+
+        switch ($root->getServerType()) {
+            case Zend_Ldap_Node_RootDse::SERVER_TYPE_ACTIVEDIRECTORY:
+                $this->assertType('string', $root->getConfigurationNamingContext());
+                $this->assertType('string', $root->getCurrentTime());
+                $this->assertType('string', $root->getDefaultNamingContext());
+                $this->assertType('string', $root->getDnsHostName());
+                $this->assertType('string', $root->getDomainControllerFunctionality());
+                $this->assertType('string', $root->getDomainFunctionality());
+                $this->assertType('string', $root->getDsServiceName());
+                $this->assertType('string', $root->getForestFunctionality());
+                $this->assertType('string', $root->getHighestCommittedUSN());
+                $this->assertType('boolean', $root->getIsGlobalCatalogReady());
+                $this->assertType('boolean', $root->getIsSynchronized());
+                $this->assertType('string', $root->getLdapServiceName());
+                $this->assertType('string', $root->getRootDomainNamingContext());
+                $this->assertType('string', $root->getSchemaNamingContext());
+                $this->assertType('string', $root->getServerName());
+                break;
+            case Zend_Ldap_Node_RootDse::SERVER_TYPE_EDIRECTORY:
+                $this->assertType('string', $root->getVendorName());
+                $this->assertType('string', $root->getVendorVersion());
+                $this->assertType('string', $root->getDsaName());
+                $this->assertType('string', $root->getStatisticsErrors());
+                $this->assertType('string', $root->getStatisticsSecurityErrors());
+                $this->assertType('string', $root->getStatisticsChainings());
+                $this->assertType('string', $root->getStatisticsReferralsReturned());
+                $this->assertType('string', $root->getStatisticsExtendedOps());
+                $this->assertType('string', $root->getStatisticsAbandonOps());
+                $this->assertType('string', $root->getStatisticsWholeSubtreeSearchOps());
+                break;
+            case Zend_Ldap_Node_RootDse::SERVER_TYPE_OPENLDAP:
+                $this->_assertNullOrString($root->getConfigContext());
+                $this->_assertNullOrString($root->getMonitorContext());
+                break;
+        }
+    }
+
+    protected function _assertNullOrString($value)
+    {
+        if ($value===null) {
+            $this->assertNull($value);
+        } else {
+            $this->assertType('string', $value);
+        }
+    }
+
+    /**
+     * @expectedException BadMethodCallException
+     */
+    public function testSetterWillThrowException()
+    {
+          $root=$this->_getLdap()->getRootDse();
+          $root->objectClass='illegal';
+    }
+
+    /**
+     * @expectedException BadMethodCallException
+     */
+    public function testOffsetSetWillThrowException()
+    {
+          $root=$this->_getLdap()->getRootDse();
+          $root['objectClass']='illegal';
+    }
+
+    /**
+     * @expectedException BadMethodCallException
+     */
+    public function testUnsetterWillThrowException()
+    {
+          $root=$this->_getLdap()->getRootDse();
+          unset($root->objectClass);
+    }
+
+    /**
+     * @expectedException BadMethodCallException
+     */
+    public function testOffsetUnsetWillThrowException()
+    {
+          $root=$this->_getLdap()->getRootDse();
+          unset($root['objectClass']);
+    }
+}

+ 312 - 0
tests/Zend/Ldap/Node/SchemaTest.php

@@ -0,0 +1,312 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Zend_Ldap_OnlineTestCase
+ */
+require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'OnlineTestCase.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+class Zend_Ldap_Node_SchemaTest extends Zend_Ldap_OnlineTestCase
+{
+    /**
+     * @var Zend_Ldap_Node_Schema
+     */
+    private $_schema;
+
+    protected function setUp()
+    {
+        parent::setUp();
+        $this->_schema=$this->_getLdap()->getSchema();
+    }
+
+    public function testSchemaNode()
+    {
+        $schema=$this->_getLdap()->getSchema();
+
+        $this->assertEquals($this->_schema, $schema);
+        $this->assertSame($this->_schema, $schema);
+
+        $serial=serialize($this->_schema);
+        $schemaUn=unserialize($serial);
+        $this->assertEquals($this->_schema, $schemaUn);
+        $this->assertNotSame($this->_schema, $schemaUn);
+    }
+
+    public function testGetters()
+    {
+        $this->assertType('array', $this->_schema->getAttributeTypes());
+        $this->assertType('array', $this->_schema->getObjectClasses());
+
+        switch ($this->_getLdap()->getRootDse()->getServerType()) {
+            case Zend_Ldap_Node_RootDse::SERVER_TYPE_ACTIVEDIRECTORY:
+                break;
+            case Zend_Ldap_Node_RootDse::SERVER_TYPE_EDIRECTORY:
+                break;
+            case Zend_Ldap_Node_RootDse::SERVER_TYPE_OPENLDAP:
+                $this->assertType('array', $this->_schema->getLdapSyntaxes());
+                $this->assertType('array', $this->_schema->getMatchingRules());
+                $this->assertType('array', $this->_schema->getMatchingRuleUse());
+                break;
+        }
+    }
+
+    /**
+     * @expectedException BadMethodCallException
+     */
+    public function testSetterWillThrowException()
+    {
+          $this->_schema->objectClass='illegal';
+    }
+
+    /**
+     * @expectedException BadMethodCallException
+     */
+    public function testOffsetSetWillThrowException()
+    {
+          $this->_schema['objectClass']='illegal';
+    }
+
+    /**
+     * @expectedException BadMethodCallException
+     */
+    public function testUnsetterWillThrowException()
+    {
+          unset($this->_schema->objectClass);
+    }
+
+    /**
+     * @expectedException BadMethodCallException
+     */
+    public function testOffsetUnsetWillThrowException()
+    {
+          unset($this->_schema['objectClass']);
+    }
+
+    public function testOpenLdapSchema()
+    {
+        if ($this->_getLdap()->getRootDse()->getServerType() !==
+                Zend_Ldap_Node_RootDse::SERVER_TYPE_OPENLDAP) {
+            $this->markTestSkipped('Test can only be run on an OpenLDAP server');
+        }
+
+        $objectClasses=$this->_schema->getObjectClasses();
+        $attributeTypes=$this->_schema->getAttributeTypes();
+
+        $this->assertArrayHasKey('organizationalUnit', $objectClasses);
+        $ou=$objectClasses['organizationalUnit'];
+        $this->assertType('Zend_Ldap_Node_Schema_ObjectClass_OpenLdap', $ou);
+        $this->assertEquals('organizationalUnit', $ou->getName());
+        $this->assertEquals('2.5.6.5', $ou->getOid());
+        $this->assertEquals(array('objectClass', 'ou'), $ou->getMustContain());
+        $this->assertEquals(array('businessCategory', 'description', 'destinationIndicator',
+            'facsimileTelephoneNumber', 'internationaliSDNNumber', 'l',
+            'physicalDeliveryOfficeName', 'postOfficeBox', 'postalAddress', 'postalCode',
+            'preferredDeliveryMethod', 'registeredAddress', 'searchGuide', 'seeAlso', 'st',
+            'street', 'telephoneNumber', 'teletexTerminalIdentifier', 'telexNumber',
+            'userPassword', 'x121Address'), $ou->getMayContain());
+        $this->assertEquals('RFC2256: an organizational unit', $ou->getDescription());
+        $this->assertEquals(Zend_Ldap_Node_Schema::OBJECTCLASS_TYPE_STRUCTURAL, $ou->getType());
+        $this->assertEquals(array('top'), $ou->getParentClasses());
+
+        $this->assertEquals('2.5.6.5', $ou->oid);
+        $this->assertEquals('organizationalUnit', $ou->name);
+        $this->assertEquals('RFC2256: an organizational unit', $ou->desc);
+        $this->assertFalse($ou->obsolete);
+        $this->assertEquals(array('top'), $ou->sup);
+        $this->assertFalse($ou->abstract);
+        $this->assertTrue($ou->structural);
+        $this->assertFalse($ou->auxiliary);
+        $this->assertEquals(array('ou'), $ou->must);
+        $this->assertEquals(array('userPassword', 'searchGuide', 'seeAlso', 'businessCategory',
+            'x121Address', 'registeredAddress', 'destinationIndicator', 'preferredDeliveryMethod',
+            'telexNumber', 'teletexTerminalIdentifier', 'telephoneNumber',
+            'internationaliSDNNumber', 'facsimileTelephoneNumber', 'street', 'postOfficeBox',
+            'postalCode', 'postalAddress', 'physicalDeliveryOfficeName', 'st', 'l',
+            'description'), $ou->may);
+        $this->assertEquals("( 2.5.6.5 NAME 'organizationalUnit' " .
+            "DESC 'RFC2256: an organizational unit' SUP top STRUCTURAL MUST ou " .
+            "MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $ x121Address $ " .
+            "registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ " .
+            "teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ " .
+            "facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ " .
+            "physicalDeliveryOfficeName $ st $ l $ description ) )", $ou->_string);
+        $this->assertEquals(array(), $ou->aliases);
+        $this->assertSame($objectClasses['top'], $ou->_parents[0]);
+
+        $this->assertArrayHasKey('ou', $attributeTypes);
+        $ou=$attributeTypes['ou'];
+        $this->assertType('Zend_Ldap_Node_Schema_AttributeType_OpenLdap', $ou);
+        $this->assertEquals('ou', $ou->getName());
+        $this->assertEquals('2.5.4.11', $ou->getOid());
+        $this->assertEquals('1.3.6.1.4.1.1466.115.121.1.15', $ou->getSyntax());
+        $this->assertEquals(32768, $ou->getMaxLength());
+        $this->assertFalse($ou->isSingleValued());
+        $this->assertEquals('RFC2256: organizational unit this object belongs to', $ou->getDescription());
+
+        $this->assertEquals('2.5.4.11', $ou->oid);
+        $this->assertEquals('ou', $ou->name);
+        $this->assertEquals('RFC2256: organizational unit this object belongs to', $ou->desc);
+        $this->assertFalse($ou->obsolete);
+        $this->assertEquals(array('name'), $ou->sup);
+        $this->assertNull($ou->equality);
+        $this->assertNull($ou->ordering);
+        $this->assertNull($ou->substr);
+        $this->assertNull($ou->syntax);
+        $this->assertNull($ou->{'max-length'});
+        $this->assertFalse($ou->{'single-value'});
+        $this->assertFalse($ou->collective);
+        $this->assertFalse($ou->{'no-user-modification'});
+        $this->assertEquals('userApplications', $ou->usage);
+        $this->assertEquals("( 2.5.4.11 NAME ( 'ou' 'organizationalUnitName' ) " .
+            "DESC 'RFC2256: organizational unit this object belongs to' SUP name )", $ou->_string);
+        $this->assertEquals(array('organizationalUnitName'), $ou->aliases);
+        $this->assertSame($attributeTypes['name'], $ou->_parents[0]);
+    }
+
+    public function testActiveDirectorySchema()
+    {
+        if ($this->_getLdap()->getRootDse()->getServerType() !==
+                Zend_Ldap_Node_RootDse::SERVER_TYPE_ACTIVEDIRECTORY) {
+            $this->markTestSkipped('Test can only be run on an Active Directory server');
+        }
+
+        $objectClasses=$this->_schema->getObjectClasses();
+        $attributeTypes=$this->_schema->getAttributeTypes();
+    }
+
+    public function testeDirectorySchema()
+    {
+        if ($this->_getLdap()->getRootDse()->getServerType() !==
+                Zend_Ldap_Node_RootDse::SERVER_TYPE_EDIRECTORY) {
+            $this->markTestSkipped('Test can only be run on an eDirectory server');
+        }
+        $this->markTestIncomplete("Novell eDirectory schema parsing is incomplete");
+    }
+
+    public function testOpenLdapSchemaAttributeTypeInheritance()
+    {
+        if ($this->_getLdap()->getRootDse()->getServerType() !==
+                Zend_Ldap_Node_RootDse::SERVER_TYPE_OPENLDAP) {
+            $this->markTestSkipped('Test can only be run on an OpenLDAP server');
+        }
+
+        $attributeTypes=$this->_schema->getAttributeTypes();
+
+        $name=$attributeTypes['name'];
+        $cn=$attributeTypes['cn'];
+
+        $this->assertEquals('2.5.4.41', $name->getOid());
+        $this->assertEquals('2.5.4.3', $cn->getOid());
+        $this->assertNull($name->sup);
+        $this->assertEquals(array('name'), $cn->sup);
+
+        $this->assertEquals('caseIgnoreMatch', $name->equality);
+        $this->assertNull($name->ordering);
+        $this->assertEquals('caseIgnoreSubstringsMatch', $name->substr);
+        $this->assertEquals('1.3.6.1.4.1.1466.115.121.1.15', $name->syntax);
+        $this->assertEquals('1.3.6.1.4.1.1466.115.121.1.15', $name->getSyntax());
+        $this->assertEquals(32768, $name->{'max-length'});
+        $this->assertEquals(32768, $name->getMaxLength());
+
+        $this->assertNull($cn->equality);
+        $this->assertNull($cn->ordering);
+        $this->assertNull($cn->substr);
+        $this->assertNull($cn->syntax);
+        $this->assertEquals('1.3.6.1.4.1.1466.115.121.1.15', $cn->getSyntax());
+        $this->assertNull($cn->{'max-length'});
+        $this->assertEquals(32768, $cn->getMaxLength());
+    }
+
+    public function testOpenLdapSchemaObjectClassInheritance()
+    {
+        if ($this->_getLdap()->getRootDse()->getServerType() !==
+                Zend_Ldap_Node_RootDse::SERVER_TYPE_OPENLDAP) {
+            $this->markTestSkipped('Test can only be run on an OpenLDAP server');
+        }
+
+        $objectClasses=$this->_schema->getObjectClasses();
+
+        if (!array_key_exists('certificationAuthority', $objectClasses) ||
+                !array_key_exists('certificationAuthority-V2', $objectClasses)) {
+            $this->markTestSkipped('This requires OpenLDAP core schema');
+        }
+
+        $ca=$objectClasses['certificationAuthority'];
+        $ca2=$objectClasses['certificationAuthority-V2'];
+
+        $this->assertEquals('2.5.6.16', $ca->getOid());
+        $this->assertEquals('2.5.6.16.2', $ca2->getOid());
+        $this->assertEquals(array('top'), $ca->sup);
+        $this->assertEquals(array('certificationAuthority'), $ca2->sup);
+
+        $this->assertEquals(array('authorityRevocationList', 'certificateRevocationList',
+            'cACertificate'), $ca->must);
+        $this->assertEquals(array('authorityRevocationList', 'cACertificate',
+            'certificateRevocationList', 'objectClass'), $ca->getMustContain());
+        $this->assertEquals(array('crossCertificatePair'), $ca->may);
+        $this->assertEquals(array('crossCertificatePair'), $ca->getMayContain());
+
+        $this->assertEquals(array(), $ca2->must);
+        $this->assertEquals(array('authorityRevocationList', 'cACertificate',
+            'certificateRevocationList', 'objectClass'), $ca2->getMustContain());
+        $this->assertEquals(array('deltaRevocationList'), $ca2->may);
+        $this->assertEquals(array('crossCertificatePair', 'deltaRevocationList'),
+            $ca2->getMayContain());
+    }
+
+    public function testOpenLdapSchemaAttributeTypeAliases()
+    {
+        if ($this->_getLdap()->getRootDse()->getServerType() !==
+                Zend_Ldap_Node_RootDse::SERVER_TYPE_OPENLDAP) {
+            $this->markTestSkipped('Test can only be run on an OpenLDAP server');
+        }
+
+        $attributeTypes=$this->_schema->getAttributeTypes();
+        $this->assertArrayHasKey('cn', $attributeTypes);
+        $this->assertArrayHasKey('commonName', $attributeTypes);
+        $ob1=$attributeTypes['cn'];
+        $ob2=$attributeTypes['commonName'];
+        $this->assertSame($ob1, $ob2);
+    }
+
+    public function testOpenLdapSchemaObjectClassAliases()
+    {
+        if ($this->_getLdap()->getRootDse()->getServerType() !==
+                Zend_Ldap_Node_RootDse::SERVER_TYPE_OPENLDAP) {
+            $this->markTestSkipped('Test can only be run on an OpenLDAP server');
+        }
+
+        $objectClasses=$this->_schema->getObjectClasses();
+        $this->assertArrayHasKey('OpenLDAProotDSE', $objectClasses);
+        $this->assertArrayHasKey('LDAProotDSE', $objectClasses);
+        $ob1=$objectClasses['OpenLDAProotDSE'];
+        $ob2=$objectClasses['LDAProotDSE'];
+        $this->assertSame($ob1, $ob2);
+    }
+}

+ 217 - 0
tests/Zend/Ldap/Node/UpdateTest.php

@@ -0,0 +1,217 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Zend_Ldap_OnlineTestCase
+ */
+require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'OnlineTestCase.php';
+/**
+ * @see Zend_Ldap_Node
+ */
+require_once 'Zend/Ldap/Node.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+class Zend_Ldap_Node_UpdateTest extends Zend_Ldap_OnlineTestCase
+{
+    protected function setUp()
+    {
+        parent::setUp();
+        $this->_prepareLdapServer();
+    }
+
+    protected function tearDown()
+    {
+        foreach ($this->_getLdap()->getBaseNode()->searchChildren('objectClass=*') as $child) {
+            $this->_getLdap()->delete($child->getDn(), true);
+        }
+
+        parent::tearDown();
+    }
+
+    protected function _stripActiveDirectorySystemAttributes(&$entry)
+    {
+        $adAttributes = array('distinguishedname', 'instancetype', 'name', 'objectcategory',
+            'objectguid', 'usnchanged', 'usncreated', 'whenchanged', 'whencreated');
+        foreach ($adAttributes as $attr) {
+            if (array_key_exists($attr, $entry)) {
+                unset($entry[$attr]);
+            }
+        }
+
+        if (array_key_exists('objectclass', $entry) && count($entry['objectclass']) > 0) {
+            if ($entry['objectclass'][0] !== 'top') {
+                $entry['objectclass']=array_merge(array('top'), $entry['objectclass']);
+            }
+        }
+    }
+
+    public function testSimpleUpdateOneValue()
+    {
+        $dn=$this->_createDn('ou=Test1,');
+        $node1=Zend_Ldap_Node::fromLdap($dn, $this->_getLdap());
+        $node1->l='f';
+        $node1->update();
+
+        $this->assertTrue($this->_getLdap()->exists($dn));
+        $node2=$this->_getLdap()->getEntry($dn);
+        $this->_stripActiveDirectorySystemAttributes($node2);
+        unset($node2['dn']);
+        $node1=$node1->getData(false);
+        $this->_stripActiveDirectorySystemAttributes($node1);
+        $this->assertEquals($node2, $node1);
+    }
+
+    public function testAddNewNode()
+    {
+        $dn=$this->_createDn('ou=Test,');
+        $node1=Zend_Ldap_Node::create($dn, array('organizationalUnit'));
+        $node1->l='a';
+        $node1->update($this->_getLdap());
+
+        $this->assertTrue($this->_getLdap()->exists($dn));
+        $node2=$this->_getLdap()->getEntry($dn);
+        $this->_stripActiveDirectorySystemAttributes($node2);
+        unset($node2['dn']);
+        $node1=$node1->getData(false);
+        $this->_stripActiveDirectorySystemAttributes($node1);
+        $this->assertEquals($node2, $node1);
+    }
+
+    public function testMoveExistingNode()
+    {
+        $dnOld=$this->_createDn('ou=Test1,');
+        $dnNew=$this->_createDn('ou=Test,');
+        $node1=Zend_Ldap_Node::fromLdap($dnOld, $this->_getLdap());
+        $node1->l='f';
+        $node1->setDn($dnNew);
+        $node1->update();
+
+        $this->assertFalse($this->_getLdap()->exists($dnOld));
+        $this->assertTrue($this->_getLdap()->exists($dnNew));
+        $node2=$this->_getLdap()->getEntry($dnNew);
+        $this->_stripActiveDirectorySystemAttributes($node2);
+        unset($node2['dn']);
+        $node1=$node1->getData(false);
+        $this->_stripActiveDirectorySystemAttributes($node1);
+        $this->assertEquals($node2, $node1);
+    }
+
+    public function testMoveNewNode()
+    {
+        $dnOld=$this->_createDn('ou=Test,');
+        $dnNew=$this->_createDn('ou=TestNew,');
+        $node1=Zend_Ldap_Node::create($dnOld, array('organizationalUnit'));
+        $node1->l='a';
+        $node1->setDn($dnNew);
+        $node1->update($this->_getLdap());
+
+        $this->assertFalse($this->_getLdap()->exists($dnOld));
+        $this->assertTrue($this->_getLdap()->exists($dnNew));
+        $node2=$this->_getLdap()->getEntry($dnNew);
+        $this->_stripActiveDirectorySystemAttributes($node2);
+        unset($node2['dn']);
+        $node1=$node1->getData(false);
+        $this->_stripActiveDirectorySystemAttributes($node1);
+        $this->assertEquals($node2, $node1);
+    }
+
+    public function testModifyDeletedNode()
+    {
+        $dn=$this->_createDn('ou=Test1,');
+        $node1=Zend_Ldap_Node::create($dn, array('organizationalUnit'));
+        $node1->delete();
+        $node1->update($this->_getLdap());
+
+        $this->assertFalse($this->_getLdap()->exists($dn));
+
+        $node1->l='a';
+        $node1->update();
+
+        $this->assertFalse($this->_getLdap()->exists($dn));
+    }
+
+    public function testAddDeletedNode()
+    {
+        $dn=$this->_createDn('ou=Test,');
+        $node1=Zend_Ldap_Node::create($dn, array('organizationalUnit'));
+        $node1->delete();
+        $node1->update($this->_getLdap());
+
+        $this->assertFalse($this->_getLdap()->exists($dn));
+    }
+
+    public function testMoveDeletedExistingNode()
+    {
+        $dnOld=$this->_createDn('ou=Test1,');
+        $dnNew=$this->_createDn('ou=Test,');
+        $node1=Zend_Ldap_Node::fromLdap($dnOld, $this->_getLdap());
+        $node1->setDn($dnNew);
+        $node1->delete();
+        $node1->update();
+
+        $this->assertFalse($this->_getLdap()->exists($dnOld));
+        $this->assertFalse($this->_getLdap()->exists($dnNew));
+    }
+
+    public function testMoveDeletedNewNode()
+    {
+        $dnOld=$this->_createDn('ou=Test,');
+        $dnNew=$this->_createDn('ou=TestNew,');
+        $node1=Zend_Ldap_Node::create($dnOld, array('organizationalUnit'));
+        $node1->setDn($dnNew);
+        $node1->delete();
+        $node1->update($this->_getLdap());
+
+        $this->assertFalse($this->_getLdap()->exists($dnOld));
+        $this->assertFalse($this->_getLdap()->exists($dnNew));
+    }
+
+    public function testMoveNode()
+    {
+        $dnOld=$this->_createDn('ou=Test1,');
+        $dnNew=$this->_createDn('ou=Test,');
+
+        $node=Zend_Ldap_Node::fromLdap($dnOld, $this->_getLdap());
+        $node->setDn($dnNew);
+        $node->update();
+        $this->assertFalse($this->_getLdap()->exists($dnOld));
+        $this->assertTrue($this->_getLdap()->exists($dnNew));
+
+        $node=Zend_Ldap_Node::fromLdap($dnNew, $this->_getLdap());
+        $node->move($dnOld);
+        $node->update();
+        $this->assertFalse($this->_getLdap()->exists($dnNew));
+        $this->assertTrue($this->_getLdap()->exists($dnOld));
+
+        $node=Zend_Ldap_Node::fromLdap($dnOld, $this->_getLdap());
+        $node->rename($dnNew);
+        $node->update();
+        $this->assertFalse($this->_getLdap()->exists($dnOld));
+        $this->assertTrue($this->_getLdap()->exists($dnNew));
+    }
+}

+ 74 - 40
tests/Zend/Ldap/OfflineTest.php

@@ -31,6 +31,10 @@ require_once dirname(__FILE__) . '/../../TestHelper.php';
  * @see Zend_Ldap
  */
 require_once 'Zend/Ldap.php';
+/**
+ * @see Zend_Ldap_Exception
+ */
+require_once 'Zend/Ldap/Exception.php';
 
 
 /**
@@ -64,16 +68,6 @@ class Zend_Ldap_OfflineTest extends PHPUnit_Framework_TestCase
     /**
      * @return void
      */
-    public function testFilterEscapeBasicOperation()
-    {
-        $input = 'a*b(b)d\e/f';
-        $expected = 'a\2ab\28b\29d\5ce\2ff';
-        $this->assertEquals($expected, Zend_Ldap::filterEscape($input));
-    }
-
-    /**
-     * @return void
-     */
     public function testInvalidOptionResultsInException()
     {
         $optionName = 'invalid';
@@ -85,36 +79,76 @@ class Zend_Ldap_OfflineTest extends PHPUnit_Framework_TestCase
         }
     }
 
-    /**
-     * @return void
-     */
-    public function testExplodeDnOperation()
+    public function testException()
     {
-		$inputs = array(
-			'CN=Alice Baker,CN=Users,DC=example,DC=com' => true,
-			'CN=Baker\\, Alice,CN=Users,DC=example,DC=com' => true,
-			'OU=Sales,DC=local' => true,
-			'OU=Sales;DC=local' => true,
-			'OU=Sales ,DC=local' => true,
-			'OU=Sales, dC=local' => true,
-			'ou=Sales , DC=local' => true,
-			'OU=Sales ; dc=local' => true,
-			'DC=local' => true,
-			' DC=local' => true,
-			'DC= local  ' => true,
-			'username' => false,
-			'username@example.com' => false,
-			'EXAMPLE\\username' => false,
-			'CN=,Alice Baker,CN=Users,DC=example,DC=com' => false,
-			'CN=Users,DC==example,DC=com' => false,
-			'O=ACME' => true,
-			'' => false,
-			'	' => false,
-		);
+        $e = new Zend_Ldap_Exception(null, '', 0);
+        $this->assertEquals('no exception message', $e->getMessage());
+        $this->assertEquals(0, $e->getCode());
+        $this->assertEquals(0, $e->getErrorCode());
 
-		foreach ($inputs as $dn => $expected) {
-			$ret = Zend_Ldap::explodeDn($dn);
-			$this->assertTrue($ret === $expected);
-		}
-	}
+        $e = new Zend_Ldap_Exception(null, '', 15);
+        $this->assertEquals('0xf: no exception message', $e->getMessage());
+        $this->assertEquals(15, $e->getCode());
+        $this->assertEquals(15, $e->getErrorCode());
+    }
+
+    public function testOptionsGetter()
+    {
+        $options = array(
+            'host' => TESTS_ZEND_LDAP_HOST,
+            'username' => TESTS_ZEND_LDAP_USERNAME,
+            'password' => TESTS_ZEND_LDAP_PASSWORD,
+            'baseDn' => TESTS_ZEND_LDAP_BASE_DN,
+        );
+        $ldap = new Zend_Ldap($options);
+        $this->assertEquals(array(
+            'host'                   => TESTS_ZEND_LDAP_HOST,
+            'port'                   => 0,
+            'useSsl'                 => false,
+            'username'               => TESTS_ZEND_LDAP_USERNAME,
+            'password'               => TESTS_ZEND_LDAP_PASSWORD,
+            'bindRequiresDn'         => false,
+            'baseDn'                 => TESTS_ZEND_LDAP_BASE_DN,
+            'accountCanonicalForm'   => null,
+            'accountDomainName'      => null,
+            'accountDomainNameShort' => null,
+            'accountFilterFormat'    => null,
+            'allowEmptyPassword'     => false,
+            'useStartTls'            => false,
+            'optReferrals'           => false,
+            'tryUsernameSplit'       => true
+        ), $ldap->getOptions());
+    }
+
+    public function testConfigObject()
+    {
+        /**
+         * @see Zend_Config
+         */
+        require_once 'Zend/Config.php';
+        $config = new Zend_Config(array(
+            'host' => TESTS_ZEND_LDAP_HOST,
+            'username' => TESTS_ZEND_LDAP_USERNAME,
+            'password' => TESTS_ZEND_LDAP_PASSWORD,
+            'baseDn' => TESTS_ZEND_LDAP_BASE_DN,
+        ));
+        $ldap = new Zend_Ldap($config);
+        $this->assertEquals(array(
+            'host'                   => TESTS_ZEND_LDAP_HOST,
+            'port'                   => 0,
+            'useSsl'                 => false,
+            'username'               => TESTS_ZEND_LDAP_USERNAME,
+            'password'               => TESTS_ZEND_LDAP_PASSWORD,
+            'bindRequiresDn'         => false,
+            'baseDn'                 => TESTS_ZEND_LDAP_BASE_DN,
+            'accountCanonicalForm'   => null,
+            'accountDomainName'      => null,
+            'accountDomainNameShort' => null,
+            'accountFilterFormat'    => null,
+            'allowEmptyPassword'     => false,
+            'useStartTls'            => false,
+            'optReferrals'           => false,
+            'tryUsernameSplit'       => true
+        ), $ldap->getOptions());
+    }
 }

+ 142 - 0
tests/Zend/Ldap/OnlineTestCase.php

@@ -0,0 +1,142 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Zend_Ldap_TestCase
+ */
+require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'TestCase.php';
+/**
+ * @see Zend_Ldap
+ */
+require_once 'Zend/Ldap.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+abstract class Zend_Ldap_OnlineTestCase extends Zend_Ldap_TestCase
+{
+    /**
+     * @var Zend_Ldap
+     */
+    private $_ldap;
+
+    /**
+     * @var array
+     */
+    private $_nodes;
+
+    /**
+     * @return Zend_Ldap
+     */
+    protected function _getLdap()
+    {
+        return $this->_ldap;
+    }
+
+    protected function setUp()
+    {
+        if (!TESTS_ZEND_LDAP_ONLINE_ENABLED) {
+            $this->markTestSkipped("Test skipped due to test configuration");
+            return;
+        }
+
+        $options = array(
+            'host'     => TESTS_ZEND_LDAP_HOST,
+            'username' => TESTS_ZEND_LDAP_USERNAME,
+            'password' => TESTS_ZEND_LDAP_PASSWORD,
+            'baseDn'   => TESTS_ZEND_LDAP_WRITEABLE_SUBTREE,
+        );
+        if (defined('TESTS_ZEND_LDAP_PORT') && TESTS_ZEND_LDAP_PORT != 389)
+            $options['port'] = TESTS_ZEND_LDAP_PORT;
+        if (defined('TESTS_ZEND_LDAP_USE_START_TLS'))
+            $options['useStartTls'] = TESTS_ZEND_LDAP_USE_START_TLS;
+        if (defined('TESTS_ZEND_LDAP_USE_SSL'))
+            $options['useSsl'] = TESTS_ZEND_LDAP_USE_SSL;
+        if (defined('TESTS_ZEND_LDAP_BIND_REQUIRES_DN'))
+            $options['bindRequiresDn'] = TESTS_ZEND_LDAP_BIND_REQUIRES_DN;
+        if (defined('TESTS_ZEND_LDAP_ACCOUNT_FILTER_FORMAT'))
+            $options['accountFilterFormat'] = TESTS_ZEND_LDAP_ACCOUNT_FILTER_FORMAT;
+        if (defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME'))
+            $options['accountDomainName'] = TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME;
+        if (defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT'))
+            $options['accountDomainNameShort'] = TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT;
+
+        $this->_ldap=new Zend_Ldap($options);
+        $this->_ldap->bind();
+    }
+
+    protected function tearDown()
+    {
+        if ($this->_ldap!==null) {
+            $this->_ldap->disconnect();
+            $this->_ldap=null;
+        }
+    }
+
+    protected function _createDn($dn)
+    {
+        if (substr($dn, -1)!==',') {
+            $dn.=',';
+        }
+        $dn = $dn . TESTS_ZEND_LDAP_WRITEABLE_SUBTREE;
+        return Zend_Ldap_Dn::fromString($dn)->toString(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER);
+    }
+
+    protected function _prepareLdapServer()
+    {
+        $this->_nodes=array(
+            $this->_createDn('ou=Node,') =>
+                array("objectClass" => "organizationalUnit", "ou" => "Node"),
+            $this->_createDn('ou=Test1,ou=Node,') =>
+                array("objectClass" => "organizationalUnit", "ou" => "Test1"),
+            $this->_createDn('ou=Test2,ou=Node,') =>
+                array("objectClass" => "organizationalUnit", "ou" => "Test2"),
+            $this->_createDn('ou=Test1,') =>
+                array("objectClass" => "organizationalUnit", "ou" => "Test1", "l" => "e"),
+            $this->_createDn('ou=Test2,') =>
+                array("objectClass" => "organizationalUnit", "ou" => "Test2", "l" => "d"),
+            $this->_createDn('ou=Test3,') =>
+                array("objectClass" => "organizationalUnit", "ou" => "Test3", "l" => "c"),
+            $this->_createDn('ou=Test4,') =>
+                array("objectClass" => "organizationalUnit", "ou" => "Test4", "l" => "b"),
+            $this->_createDn('ou=Test5,') =>
+                array("objectClass" => "organizationalUnit", "ou" => "Test5", "l" => "a"),
+        );
+
+        $ldap=$this->_ldap->getResource();
+        foreach ($this->_nodes as $dn => $entry) {
+            ldap_add($ldap, $dn, $entry);
+        }
+    }
+
+    protected function _cleanupLdapServer()
+    {
+        $ldap=$this->_ldap->getResource();
+        foreach (array_reverse($this->_nodes) as $dn => $entry) {
+            ldap_delete($ldap, $dn);
+        }
+    }
+}

+ 184 - 0
tests/Zend/Ldap/OriginalBindTest.php

@@ -0,0 +1,184 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * Test helper
+ */
+require_once dirname(__FILE__) . '/../../TestHelper.php';
+
+/**
+ * Zend_Ldap
+ */
+require_once 'Zend/Ldap.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_OriginalBindTest extends PHPUnit_Framework_TestCase
+{
+    protected $_options = null;
+    protected $_principalName = TESTS_ZEND_LDAP_PRINCIPAL_NAME;
+    protected $_altUsername = TESTS_ZEND_LDAP_PRINCIPAL_NAME;
+    protected $_bindRequiresDn = false;
+
+    public function setUp()
+    {
+        $this->_options = array(
+            'host' => TESTS_ZEND_LDAP_HOST,
+            'username' => TESTS_ZEND_LDAP_USERNAME,
+            'password' => TESTS_ZEND_LDAP_PASSWORD,
+            'baseDn' => TESTS_ZEND_LDAP_BASE_DN,
+        );
+        if (defined('TESTS_ZEND_LDAP_PORT') && TESTS_ZEND_LDAP_PORT != 389)
+            $this->_options['port'] = TESTS_ZEND_LDAP_PORT;
+        if (defined('TESTS_ZEND_LDAP_USE_START_TLS'))
+            $this->_options['useStartTls'] = TESTS_ZEND_LDAP_USE_START_TLS;
+        if (defined('TESTS_ZEND_LDAP_USE_SSL'))
+            $this->_options['useSsl'] = TESTS_ZEND_LDAP_USE_SSL;
+        if (defined('TESTS_ZEND_LDAP_BIND_REQUIRES_DN'))
+            $this->_options['bindRequiresDn'] = TESTS_ZEND_LDAP_BIND_REQUIRES_DN;
+        if (defined('TESTS_ZEND_LDAP_ALT_USERNAME'))
+            $this->_altUsername = TESTS_ZEND_LDAP_ALT_USERNAME;
+
+        if (isset($this->_options['bindRequiresDn']))
+            $this->_bindRequiresDn = $this->_options['bindRequiresDn'];
+    }
+
+    public function testEmptyOptionsBind()
+    {
+        $ldap = new Zend_Ldap(array());
+        try {
+            $ldap->bind();
+            $this->fail('Expected exception for empty options');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertContains('A host parameter is required', $zle->getMessage());
+        }
+    }
+    public function testAnonymousBind()
+    {
+        $options = $this->_options;
+        unset($options['password']);
+
+        $ldap = new Zend_Ldap($options);
+        try {
+            $ldap->bind();
+        } catch (Zend_Ldap_Exception $zle) {
+            // or I guess the server doesn't allow unauthenticated binds
+            $this->assertContains('unauthenticated bind', $zle->getMessage());
+        }
+    }
+    public function testNoBaseDnBind()
+    {
+        $options = $this->_options;
+        unset($options['baseDn']);
+        $options['bindRequiresDn'] = true;
+
+        $ldap = new Zend_Ldap($options);
+        try {
+            $ldap->bind('invalid', 'ignored');
+            $this->fail('Expected exception for baseDn missing');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertContains('Base DN not set', $zle->getMessage());
+        }
+    }
+    public function testNoDomainNameBind()
+    {
+        $options = $this->_options;
+        unset($options['baseDn']);
+        $options['bindRequiresDn'] = false;
+
+        $ldap = new Zend_Ldap($options);
+        try {
+            $ldap->bind('invalid', 'ignored');
+            $this->fail('Expected exception for missing accountDomainName');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertContains('Option required: accountDomainName', $zle->getMessage());
+        }
+    }
+    public function testPlainBind()
+    {
+        $ldap = new Zend_Ldap($this->_options);
+        $ldap->bind();
+    }
+    public function testConnectBind()
+    {
+        $ldap = new Zend_Ldap($this->_options);
+        $ldap->connect()->bind();
+    }
+    public function testExplicitParamsBind()
+    {
+        $options = $this->_options;
+        $username = $options['username'];
+        $password = $options['password'];
+
+        unset($options['username']);
+        unset($options['password']);
+
+        $ldap = new Zend_Ldap($options);
+        $ldap->bind($username, $password);
+    }
+    public function testRequiresDnBind()
+    {
+        $options = $this->_options;
+
+        /* Fixup filter since bindRequiresDn is used to determine default accountFilterFormat
+         */
+        if (!isset($options['accountFilterFormat']) && $this->_bindRequiresDn === false)
+            $options['accountFilterFormat'] = '(&(objectClass=user)(sAMAccountName=%s))';
+
+        $options['bindRequiresDn'] = true;
+
+        $ldap = new Zend_Ldap($options);
+        try {
+            $ldap->bind($this->_altUsername, 'invalid');
+        } catch (Zend_Ldap_Exception $zle) {
+            $message = str_replace("\n", " ", $zle->getMessage());
+            $this->assertContains('Invalid credentials', $message);
+        }
+    }
+    public function testRequiresDnWithoutDnBind()
+    {
+        $options = $this->_options;
+
+        /* Fixup filter since bindRequiresDn is used to determine default accountFilterFormat
+         */
+        if (!isset($options['accountFilterFormat']) && !$this->_bindRequiresDn)
+            $options['accountFilterFormat'] = '(&(objectClass=user)(sAMAccountName=%s))';
+
+        $options['bindRequiresDn'] = true;
+
+        unset($options['username']);
+
+        $ldap = new Zend_Ldap($options);
+        try {
+            $ldap->bind($this->_principalName);
+        } catch (Zend_Ldap_Exception $zle) {
+            /* Note that if your server actually allows anonymous binds this test will fail.
+             */
+            $this->assertContains('Failed to retrieve DN', $zle->getMessage());
+        }
+    }
+}

+ 123 - 0
tests/Zend/Ldap/OriginalCanonTest.php

@@ -0,0 +1,123 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * Test helper
+ */
+require_once dirname(__FILE__) . '/../../TestHelper.php';
+
+/**
+ * Zend_Ldap
+ */
+require_once 'Zend/Ldap.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_OriginalCanonTest extends PHPUnit_Framework_TestCase
+{
+    protected $_options = null;
+    protected $_principalName = TESTS_ZEND_LDAP_PRINCIPAL_NAME;
+    protected $_names = array();
+
+    public function setUp()
+    {
+        $this->_options = array(
+            'host' => TESTS_ZEND_LDAP_HOST,
+            'username' => TESTS_ZEND_LDAP_USERNAME,
+            'password' => TESTS_ZEND_LDAP_PASSWORD,
+            'baseDn' => TESTS_ZEND_LDAP_BASE_DN,
+        );
+        if (defined('TESTS_ZEND_LDAP_PORT') && TESTS_ZEND_LDAP_PORT != 389)
+            $this->_options['port'] = TESTS_ZEND_LDAP_PORT;
+        if (defined('TESTS_ZEND_LDAP_USE_SSL'))
+            $this->_options['useSsl'] = TESTS_ZEND_LDAP_USE_SSL;
+        if (defined('TESTS_ZEND_LDAP_BIND_REQUIRES_DN'))
+            $this->_options['bindRequiresDn'] = TESTS_ZEND_LDAP_BIND_REQUIRES_DN;
+        if (defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME'))
+            $this->_options['accountDomainName'] = TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME;
+        if (defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT'))
+            $this->_options['accountDomainNameShort'] = TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT;
+        if (defined('TESTS_ZEND_LDAP_ALT_USERNAME')) {
+            $this->_names[Zend_Ldap::ACCTNAME_FORM_USERNAME] = TESTS_ZEND_LDAP_ALT_USERNAME;
+            if (defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME')) {
+                $this->_names[Zend_Ldap::ACCTNAME_FORM_PRINCIPAL] =
+                    TESTS_ZEND_LDAP_ALT_USERNAME . '@' . TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME;
+            }
+            if (defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT')) {
+                $this->_names[Zend_Ldap::ACCTNAME_FORM_BACKSLASH] =
+                    TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT . '\\' . TESTS_ZEND_LDAP_ALT_USERNAME;
+            }
+        }
+    }
+
+    public function testPlainCanon()
+    {
+        $ldap = new Zend_Ldap($this->_options);
+
+        /* This test tries to canonicalize each name (uname, uname@example.com,
+         * EXAMPLE\uname) to each of the 3 forms (username, principal and backslash)
+         * for a total of canonicalizations.
+         */
+
+        foreach ($this->_names as $_form => $name) {
+            foreach ($this->_names as $form => $_name) {
+                $ret = $ldap->getCanonicalAccountName($name, $form);
+                $this->assertTrue($ret === $this->_names[$form]);
+            }
+        }
+    }
+    public function testInvalidAccountCanon()
+    {
+        $ldap = new Zend_Ldap($this->_options);
+        try {
+            $ldap->bind('invalid', 'invalid');
+        } catch (Zend_Ldap_Exception $zle) {
+            $msg = $zle->getMessage();
+            $this->assertTrue(strstr($msg, 'Invalid credentials') || strstr($msg, 'No such object'));
+        }
+    }
+    public function testDnCanon()
+    {
+        $ldap = new Zend_Ldap($this->_options);
+        $name = $ldap->getCanonicalAccountName(TESTS_ZEND_LDAP_ALT_DN, Zend_Ldap::ACCTNAME_FORM_DN);
+    }
+    public function testMismatchDomainBind()
+    {
+        $ldap = new Zend_Ldap($this->_options);
+        try {
+            $ldap->bind('BOGUS\\doesntmatter', 'doesntmatter');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertTrue($zle->getCode() == Zend_Ldap_Exception::LDAP_X_DOMAIN_MISMATCH);
+        }
+    }
+    public function testBindCanon()
+    {
+        /**
+         * @todo test accountCanonicalForm option
+         */
+    }
+}

+ 178 - 0
tests/Zend/Ldap/OriginalConnectTest.php

@@ -0,0 +1,178 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * Test helper
+ */
+require_once dirname(__FILE__) . '/../../TestHelper.php';
+
+/**
+ * Zend_Ldap
+ */
+require_once 'Zend/Ldap.php';
+
+/* Note: The ldap_connect function does not actually try to connect. This
+ * is why many tests attempt to bind with invalid credentials. If the
+ * bind returns 'Invalid credentials' we know the transport related work
+ * was successful.
+ */
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_OriginalConnectTest extends PHPUnit_Framework_TestCase
+{
+    protected $_options = null;
+
+    public function setUp()
+    {
+        $this->_options = array('host' => TESTS_ZEND_LDAP_HOST);
+        if (defined('TESTS_ZEND_LDAP_PORT') && TESTS_ZEND_LDAP_PORT != 389)
+            $this->_options['port'] = TESTS_ZEND_LDAP_PORT;
+        if (defined('TESTS_ZEND_LDAP_USE_SSL'))
+            $this->_options['useSsl'] = TESTS_ZEND_LDAP_USE_SSL;
+    }
+
+    public function testEmptyOptionsConnect()
+    {
+        $ldap = new Zend_Ldap(array());
+        try {
+            $ldap->connect();
+            $this->fail('Expected exception for empty options');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertContains('host parameter is required', $zle->getMessage());
+        }
+    }
+    public function testUnknownHostConnect()
+    {
+        $ldap = new Zend_Ldap(array('host' => 'bogus.example.com'));
+        try {
+            // connect doesn't actually try to connect until bind is called
+            $ldap->connect()->bind('CN=ignored,DC=example,DC=com', 'ignored');
+            $this->fail('Expected exception for unknown host');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertContains('Can\'t contact LDAP server', $zle->getMessage());
+        }
+    }
+    public function testPlainConnect()
+    {
+        $ldap = new Zend_Ldap($this->_options);
+        try {
+            // Connect doesn't actually try to connect until bind is called
+            // but if we get 'Invalid credentials' then we know the connect
+            // succeeded.
+            $ldap->connect()->bind('CN=ignored,DC=example,DC=com', 'ignored');
+            $this->fail('Expected exception for invalid username');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertContains('Invalid credentials', $zle->getMessage());
+        }
+    }
+    public function testExplicitParamsConnect()
+    {
+        $host = TESTS_ZEND_LDAP_HOST;
+        $port = 0;
+        if (defined('TESTS_ZEND_LDAP_PORT') && TESTS_ZEND_LDAP_PORT != 389)
+            $port = TESTS_ZEND_LDAP_PORT;
+        $useSsl = false;
+        if (defined('TESTS_ZEND_LDAP_USE_SSL'))
+            $useSsl = TESTS_ZEND_LDAP_USE_SSL;
+
+        $ldap = new Zend_Ldap();
+        try {
+            $ldap->connect($host, $port, $useSsl)
+                 ->bind('CN=ignored,DC=example,DC=com', 'ignored');
+            $this->fail('Expected exception for invalid username');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertContains('Invalid credentials', $zle->getMessage());
+        }
+    }
+    public function testExplicitPortConnect()
+    {
+        $port = 389;
+        if (defined('TESTS_ZEND_LDAP_PORT') && TESTS_ZEND_LDAP_PORT)
+            $port = TESTS_ZEND_LDAP_PORT;
+        if (defined('TESTS_ZEND_LDAP_USE_SSL') && TESTS_ZEND_LDAP_USE_SSL)
+            $port = 636;
+
+        $ldap = new Zend_Ldap($this->_options);
+        try {
+            $ldap->connect(null, $port)
+                 ->bind('CN=ignored,DC=example,DC=com', 'ignored');
+            $this->fail('Expected exception for invalid username');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertContains('Invalid credentials', $zle->getMessage());
+        }
+    }
+    public function testBadPortConnect()
+    {
+        $options = $this->_options;
+        $options['port'] = 10;
+
+        $ldap = new Zend_Ldap($options);
+        try {
+            $ldap->connect()->bind('CN=ignored,DC=example,DC=com', 'ignored');
+            $this->fail('Expected exception for unknown username');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertContains('Can\'t contact LDAP server', $zle->getMessage());
+        }
+    }
+    public function testSetOptionsConnect()
+    {
+        $ldap = new Zend_Ldap();
+        $ldap->setOptions($this->_options);
+        try {
+            $ldap->connect()->bind('CN=ignored,DC=example,DC=com', 'ignored');
+            $this->fail('Expected exception for invalid username');
+        } catch (Zend_Ldap_Exception $zle) {
+            $this->assertContains('Invalid credentials', $zle->getMessage());
+        }
+    }
+    public function testMultiConnect()
+    {
+        $ldap = new Zend_Ldap($this->_options);
+        for ($i = 0; $i < 3; $i++) {
+            try {
+                $ldap->connect()->bind('CN=ignored,DC=example,DC=com', 'ignored');
+                $this->fail('Expected exception for unknown username');
+            } catch (Zend_Ldap_Exception $zle) {
+                $this->assertContains('Invalid credentials', $zle->getMessage());
+            }
+        }
+    }
+    public function testDisconnect()
+    {
+        $ldap = new Zend_Ldap($this->_options);
+        for ($i = 0; $i < 3; $i++) {
+            $ldap->disconnect();
+            try {
+                $ldap->connect()->bind('CN=ignored,DC=example,DC=com', 'ignored');
+                $this->fail('Expected exception for unknown username');
+            } catch (Zend_Ldap_Exception $zle) {
+                $this->assertContains('Invalid credentials', $zle->getMessage());
+            }
+        }
+    }
+}

+ 117 - 0
tests/Zend/Ldap/OriginalOfflineTest.php

@@ -0,0 +1,117 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+/**
+ * Test helper
+ */
+require_once dirname(__FILE__) . '/../../TestHelper.php';
+
+/**
+ * Zend_Ldap
+ */
+require_once 'Zend/Ldap.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Ldap_OriginalOfflineTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * Zend_Ldap instance
+     *
+     * @var Zend_Ldap
+     */
+    protected $_ldap = null;
+
+    /**
+     * Setup operations run prior to each test method:
+     *
+     * * Creates an instance of Zend_Ldap
+     *
+     * @return void
+     */
+    public function setUp()
+    {
+        $this->_ldap = new Zend_Ldap();
+    }
+
+    /**
+     * @return void
+     */
+    public function testFilterEscapeBasicOperation()
+    {
+        $input = 'a*b(b)d\e/f';
+        $expected = 'a\2ab\28b\29d\5ce\2ff';
+        $this->assertEquals($expected, Zend_Ldap::filterEscape($input));
+    }
+
+    /**
+     * @return void
+     */
+    public function testInvalidOptionResultsInException()
+    {
+        $optionName = 'invalid';
+        try {
+            $this->_ldap->setOptions(array($optionName => 'irrelevant'));
+            $this->fail('Expected Zend_Ldap_Exception not thrown');
+        } catch (Zend_Ldap_Exception $e) {
+            $this->assertEquals("Unknown Zend_Ldap option: $optionName", $e->getMessage());
+        }
+    }
+
+    /**
+     * @return void
+     */
+    public function testExplodeDnOperation()
+    {
+        $inputs = array(
+            'CN=Alice Baker,CN=Users,DC=example,DC=com' => true,
+            'CN=Baker\\, Alice,CN=Users,DC=example,DC=com' => true,
+            'OU=Sales,DC=local' => true,
+            'OU=Sales;DC=local' => true,
+            'OU=Sales ,DC=local' => true,
+            'OU=Sales, dC=local' => true,
+            'ou=Sales , DC=local' => true,
+            'OU=Sales ; dc=local' => true,
+            'DC=local' => true,
+            ' DC=local' => true,
+            'DC= local  ' => true,
+            'username' => false,
+            'username@example.com' => false,
+            'EXAMPLE\\username' => false,
+            'CN=,Alice Baker,CN=Users,DC=example,DC=com' => false,
+            'CN=Users,DC==example,DC=com' => false,
+            'O=ACME' => true,
+            '' => false,
+            '   ' => false,
+        );
+
+        foreach ($inputs as $dn => $expected) {
+            $ret = Zend_Ldap::explodeDn($dn);
+            $this->assertTrue($ret === $expected);
+        }
+    }
+}

+ 303 - 0
tests/Zend/Ldap/SearchTest.php

@@ -0,0 +1,303 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Zend_Ldap_OnlineTestCase
+ */
+require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'OnlineTestCase.php';
+
+/**
+ * @see Zend_Ldap_Dn
+ */
+require_once 'Zend/Ldap/Dn.php';
+/**
+ * @see Zend_Ldap_Filter
+ */
+require_once 'Zend/Ldap/Filter.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+class Zend_Ldap_SearchTest extends Zend_Ldap_OnlineTestCase
+{
+    protected function setUp()
+    {
+        parent::setUp();
+        $this->_prepareLdapServer();
+    }
+
+    protected function tearDown()
+    {
+        $this->_cleanupLdapServer();
+        parent::tearDown();
+    }
+
+    public function testGetSingleEntry()
+    {
+        $dn=$this->_createDn('ou=Test1,');
+        $entry=$this->_getLdap()->getEntry($dn);
+        $this->assertEquals($dn, $entry["dn"]);
+        $this->assertArrayHasKey('ou', $entry);
+        $this->assertContains('Test1', $entry['ou']);
+        $this->assertEquals(1, count($entry['ou']));
+    }
+
+    public function testGetSingleIllegalEntry()
+    {
+        $dn=$this->_createDn('ou=Test99,');
+        $entry=$this->_getLdap()->getEntry($dn);
+        $this->assertNull($entry);
+    }
+
+    /**
+     * @expectedException Zend_Ldap_Exception
+     */
+    public function testGetSingleIllegalEntryWithException()
+    {
+        $dn=$this->_createDn('ou=Test99,');
+        $entry=$this->_getLdap()->getEntry($dn, array(), true);
+    }
+
+    public function testCountBase()
+    {
+        $dn=$this->_createDn('ou=Node,');
+        $count=$this->_getLdap()->count('(objectClass=*)', $dn, Zend_Ldap::SEARCH_SCOPE_BASE);
+        $this->assertEquals(1, $count);
+    }
+
+    public function testCountOne()
+    {
+        $dn1=$this->_createDn('ou=Node,');
+        $count1=$this->_getLdap()->count('(objectClass=*)', $dn1, Zend_Ldap::SEARCH_SCOPE_ONE);
+        $this->assertEquals(2, $count1);
+        $dn2=TESTS_ZEND_LDAP_WRITEABLE_SUBTREE;
+        $count2=$this->_getLdap()->count('(objectClass=*)', $dn2, Zend_Ldap::SEARCH_SCOPE_ONE);
+        $this->assertEquals(6, $count2);
+    }
+
+    public function testCountSub()
+    {
+        $dn1=$this->_createDn('ou=Node,');
+        $count1=$this->_getLdap()->count('(objectClass=*)', $dn1, Zend_Ldap::SEARCH_SCOPE_SUB);
+        $this->assertEquals(3, $count1);
+        $dn2=TESTS_ZEND_LDAP_WRITEABLE_SUBTREE;
+        $count2=$this->_getLdap()->count('(objectClass=*)', $dn2, Zend_Ldap::SEARCH_SCOPE_SUB);
+        $this->assertEquals(9, $count2);
+    }
+
+    public function testResultIteration()
+    {
+        $items=$this->_getLdap()->search('(objectClass=organizationalUnit)',
+            TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, Zend_Ldap::SEARCH_SCOPE_SUB);
+        $this->assertEquals(9, $items->count());
+
+        $i=0;
+        foreach ($items as $key => $item)
+        {
+            $this->assertEquals($i, $key);
+            $i++;
+        }
+        $this->assertEquals(9, $i);
+        $j=0;
+        foreach ($items as $item) { $j++; }
+        $this->assertEquals($i, $j);
+    }
+
+    public function testSearchNoResult()
+    {
+        $items=$this->_getLdap()->search('(objectClass=account)', TESTS_ZEND_LDAP_WRITEABLE_SUBTREE,
+            Zend_Ldap::SEARCH_SCOPE_SUB);
+        $this->assertEquals(0, $items->count());
+    }
+
+    public function testSearchEntriesShortcut()
+    {
+        $entries=$this->_getLdap()->searchEntries('(objectClass=organizationalUnit)',
+            TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, Zend_Ldap::SEARCH_SCOPE_SUB);
+        $this->assertType("array", $entries);
+        $this->assertEquals(9, count($entries));
+    }
+
+    /**
+     * @expectedException Zend_Ldap_Exception
+     */
+    public function testIllegalSearch()
+    {
+        $dn=$this->_createDn('ou=Node2,');
+        $items=$this->_getLdap()->search('(objectClass=account)', $dn, Zend_Ldap::SEARCH_SCOPE_SUB);
+    }
+
+    public function testSearchNothingGetFirst()
+    {
+        $entries=$this->_getLdap()->search('(objectClass=account)', TESTS_ZEND_LDAP_WRITEABLE_SUBTREE,
+            Zend_Ldap::SEARCH_SCOPE_SUB);
+        $this->assertEquals(0, $entries->count());
+        $this->assertNull($entries->getFirst());
+    }
+
+    public function testSorting()
+    {
+        $lSorted=array('a', 'b', 'c', 'd', 'e');
+        $items=$this->_getLdap()->search('(l=*)', TESTS_ZEND_LDAP_WRITEABLE_SUBTREE,
+            Zend_Ldap::SEARCH_SCOPE_SUB, array(), 'l');
+        $this->assertEquals(5, $items->count());
+        foreach ($items as $key => $item)
+        {
+            $this->assertEquals($lSorted[$key], $item['l'][0]);
+        }
+    }
+
+    public function testCountChildren()
+    {
+        $dn1=$this->_createDn('ou=Node,');
+        $count1=$this->_getLdap()->countChildren($dn1);
+        $this->assertEquals(2, $count1);
+        $dn2=TESTS_ZEND_LDAP_WRITEABLE_SUBTREE;
+        $count2=$this->_getLdap()->countChildren($dn2);
+        $this->assertEquals(6, $count2);
+    }
+
+    public function testExistsDn()
+    {
+        $dn1=$this->_createDn('ou=Test2,');
+        $dn2=$this->_createDn('ou=Test99,');
+        $this->assertTrue($this->_getLdap()->exists($dn1));
+        $this->assertFalse($this->_getLdap()->exists($dn2));
+    }
+
+    public function testSearchWithDnObjectAndFilterObject()
+    {
+        $dn=Zend_Ldap_Dn::fromString(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE);
+        $filter=Zend_Ldap_Filter::equals('objectClass', 'organizationalUnit');
+
+        $items=$this->_getLdap()->search($filter, $dn, Zend_Ldap::SEARCH_SCOPE_SUB);
+        $this->assertEquals(9, $items->count());
+    }
+
+    public function testCountSubWithDnObjectAndFilterObject()
+    {
+        $dn1=Zend_Ldap_Dn::fromString($this->_createDn('ou=Node,'));
+        $filter=Zend_Ldap_Filter::any('objectClass');
+
+        $count1=$this->_getLdap()->count($filter, $dn1, Zend_Ldap::SEARCH_SCOPE_SUB);
+        $this->assertEquals(3, $count1);
+
+        $dn2=Zend_Ldap_Dn::fromString(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE);
+        $count2=$this->_getLdap()->count($filter, $dn2, Zend_Ldap::SEARCH_SCOPE_SUB);
+        $this->assertEquals(9, $count2);
+    }
+
+    public function testCountChildrenWithDnObject()
+    {
+        $dn1=Zend_Ldap_Dn::fromString($this->_createDn('ou=Node,'));
+        $count1=$this->_getLdap()->countChildren($dn1);
+        $this->assertEquals(2, $count1);
+
+        $dn2=Zend_Ldap_Dn::fromString(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE);
+        $count2=$this->_getLdap()->countChildren($dn2);
+        $this->assertEquals(6, $count2);
+    }
+
+    public function testExistsDnWithDnObject()
+    {
+        $dn1=Zend_Ldap_Dn::fromString($this->_createDn('ou=Test2,'));
+        $dn2=Zend_Ldap_Dn::fromString($this->_createDn('ou=Test99,'));
+
+        $this->assertTrue($this->_getLdap()->exists($dn1));
+        $this->assertFalse($this->_getLdap()->exists($dn2));
+    }
+
+    public function testSearchEntriesShortcutWithDnObjectAndFilterObject()
+    {
+        $dn=Zend_Ldap_Dn::fromString(TESTS_ZEND_LDAP_WRITEABLE_SUBTREE);
+        $filter=Zend_Ldap_Filter::equals('objectClass', 'organizationalUnit');
+
+        $entries=$this->_getLdap()->searchEntries($filter, $dn, Zend_Ldap::SEARCH_SCOPE_SUB);
+        $this->assertType("array", $entries);
+        $this->assertEquals(9, count($entries));
+    }
+
+    public function testGetSingleEntryWithDnObject()
+    {
+        $dn=Zend_Ldap_Dn::fromString($this->_createDn('ou=Test1,'));
+        $entry=$this->_getLdap()->getEntry($dn);
+        $this->assertEquals($dn->toString(), $entry["dn"]);
+    }
+
+    public function testMultipleResultIteration()
+    {
+        $items=$this->_getLdap()->search('(objectClass=organizationalUnit)',
+            TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, Zend_Ldap::SEARCH_SCOPE_SUB);
+        $isCount = 9;
+        $this->assertEquals($isCount, $items->count());
+
+        $i=0;
+        foreach ($items as $key => $item)
+        {
+            $this->assertEquals($i, $key);
+            $i++;
+        }
+        $this->assertEquals($isCount, $i);
+        $i=0;
+        foreach ($items as $key => $item)
+        {
+            $this->assertEquals($i, $key);
+            $i++;
+        }
+        $this->assertEquals($isCount, $i);
+
+        $items->close();
+        $i=0;
+        foreach ($items as $key => $item)
+        {
+            $this->assertEquals($i, $key);
+            $i++;
+        }
+        $this->assertEquals($isCount, $i);
+        $i=0;
+        foreach ($items as $key => $item)
+        {
+            $this->assertEquals($i, $key);
+            $i++;
+        }
+        $this->assertEquals($isCount, $i);
+    }
+
+    /**
+     * Test issue reported by Lance Hendrix on
+     * http://framework.zend.com/wiki/display/ZFPROP/Zend_Ldap+-+Extended+support+-+Stefan+Gehrig?
+     *      focusedCommentId=13107431#comment-13107431
+     */
+    public function testCallingNextAfterIterationShouldNotThrowException()
+    {
+        $items = $this->_getLdap()->search('(objectClass=organizationalUnit)',
+            TESTS_ZEND_LDAP_WRITEABLE_SUBTREE, Zend_Ldap::SEARCH_SCOPE_SUB);
+        foreach ($items as $key => $item) {
+            // do nothing - just iterate
+        }
+        $items->next();
+    }
+}

+ 64 - 0
tests/Zend/Ldap/TestCase.php

@@ -0,0 +1,64 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Test helper
+ */
+require_once dirname(dirname(dirname(__FILE__))) . DIRECTORY_SEPARATOR . 'TestHelper.php';
+/**
+ * @see Zend_Ldap_Node
+ */
+require_once 'Zend/Ldap/Node.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Ldap
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+abstract class Zend_Ldap_TestCase extends PHPUnit_Framework_TestCase
+{
+    /**
+     * @return array
+     */
+    protected function _createTestArrayData()
+    {
+        $data=array(
+            'dn'          => 'cn=name,dc=example,dc=org',
+            'cn'          => array('name'),
+            'host'        => array('a', 'b', 'c'),
+            'empty'       => array(),
+            'boolean'     => array('TRUE', 'FALSE'),
+            'objectclass' => array('account', 'top'),
+        );
+        return $data;
+    }
+
+    /**
+     * @return Zend_Ldap_Node
+     */
+    protected function _createTestNode()
+    {
+        return Zend_Ldap_Node::fromArray($this->_createTestArrayData(), true);
+    }
+}

+ 1 - 0
tests/Zend/Ldap/_files/AttributeTest.input.txt

@@ -0,0 +1 @@
+String from file