1<?php
2/**
3 * Copyright 2010-2017 Horde LLC (http://www.horde.org/)
4 *
5 * @package    Ldap
6 * @subpackage UnitTests
7 * @author     Jan Schneider <jan@horde.org>
8 * @license    http://www.gnu.org/licenses/lgpl-3.0.html LGPL-3.0
9 */
10class Horde_Ldap_UtilTest extends Horde_Test_Case
11{
12    /**
13     * Test escapeDNValue()
14     */
15    public function testEscapeDNValue()
16    {
17        $dnval    = '  ' . chr(22) . ' t,e+s"t,\\v<a>l;u#e=!    ';
18        $expected = '\20\20\16 t\,e\+s\"t\,\\\\v\<a\>l\;u\#e\=!\20\20\20\20';
19
20        // String call.
21        $this->assertEquals(
22            array($expected),
23            Horde_Ldap_Util::escapeDNValue($dnval));
24
25        // Array call.
26        $this->assertEquals(
27            array($expected),
28            Horde_Ldap_Util::escapeDNValue(array($dnval)));
29
30        // Multiple arrays.
31        $this->assertEquals(
32            array($expected, $expected, $expected),
33            Horde_Ldap_Util::escapeDNValue(array($dnval, $dnval, $dnval)));
34    }
35
36    /**
37     * Test unescapeDNValue()
38     */
39    public function testUnescapeDNValue()
40    {
41        $dnval    = '\\20\\20\\16\\20t\\,e\\+s \\"t\\,\\\\v\\<a\\>l\\;u\\#e\\=!\\20\\20\\20\\20';
42        $expected = '  ' . chr(22) . ' t,e+s "t,\\v<a>l;u#e=!    ';
43
44        // String call.
45        $this->assertEquals(
46            array($expected),
47            Horde_Ldap_Util::unescapeDNValue($dnval));
48
49        // Array call.
50        $this->assertEquals(
51            array($expected),
52            Horde_Ldap_Util::unescapeDNValue(array($dnval)));
53
54        // Multiple arrays.
55        $this->assertEquals(
56            array($expected, $expected, $expected),
57            Horde_Ldap_Util::unescapeDNValue(array($dnval, $dnval, $dnval)));
58    }
59
60    /**
61     * Test escaping of filter values.
62     */
63    public function testEscapeFilterValue()
64    {
65        $expected  = 't\28e,s\29t\2av\5cal\1eue';
66        $filterval = 't(e,s)t*v\\al' . chr(30) . 'ue';
67
68        // String call
69        $this->assertEquals(
70            array($expected),
71            Horde_Ldap_Util::escapeFilterValue($filterval));
72
73        // Array call.
74        $this->assertEquals(
75            array($expected),
76            Horde_Ldap_Util::escapeFilterValue(array($filterval)));
77
78        // Multiple arrays.
79        $this->assertEquals(
80            array($expected, $expected, $expected),
81            Horde_Ldap_Util::escapeFilterValue(array($filterval, $filterval, $filterval)));
82    }
83
84    /**
85     * Test unescaping of filter values.
86     */
87    public function testUnescapeFilterValue()
88    {
89        $expected  = 't(e,s)t*v\\al' . chr(30) . 'ue';
90        $filterval = 't\28e,s\29t\2av\5cal\1eue';
91
92        // String call
93        $this->assertEquals(
94            array($expected),
95            Horde_Ldap_Util::unescapeFilterValue($filterval));
96
97        // Array call.
98        $this->assertEquals(
99            array($expected),
100            Horde_Ldap_Util::unescapeFilterValue(array($filterval)));
101
102        // Multiple arrays.
103        $this->assertEquals(
104            array($expected, $expected, $expected),
105            Horde_Ldap_Util::unescapeFilterValue(array($filterval, $filterval, $filterval)));
106    }
107
108    /**
109     * Test asc2hex32()
110     */
111    public function testAsc2hex32()
112    {
113        $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{|}~';
114        $str = '';
115        for ($i = 0; $i < 127; $i++) {
116             $str .= chr($i);
117        }
118        $this->assertEquals($expected, Horde_Ldap_Util::asc2hex32($str));
119    }
120
121    /**
122     * Test HEX unescaping
123     */
124    public function testHex2asc()
125    {
126        $expected = '';
127        for ($i = 0; $i < 127; $i++) {
128             $expected .= chr($i);
129        }
130        $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[\]^_`abcdefghijklmnopqrstuvwxyz{|}~';
131        $this->assertEquals($expected, Horde_Ldap_Util::hex2asc($str));
132    }
133
134    /**
135     * Tests splitRDNMultivalue()
136     *
137     * In addition to the above test of the basic split correction, we test
138     * here the functionality of multivalued RDNs.
139     */
140    public function testSplitRDNMultivalue()
141    {
142        // One value.
143        $rdn = 'CN=J. Smith';
144        $expected = array('CN=J. Smith');
145        $split = Horde_Ldap_Util::splitRDNMultivalue($rdn);
146        $this->assertEquals($expected, $split);
147
148        // Two values.
149        $rdn = 'OU=Sales+CN=J. Smith';
150        $expected = array('OU=Sales', 'CN=J. Smith');
151        $split = Horde_Ldap_Util::splitRDNMultivalue($rdn);
152        $this->assertEquals($expected, $split);
153
154        // Several multivals.
155        $rdn = 'OU=Sales+CN=J. Smith+L=London+C=England';
156        $expected = array('OU=Sales', 'CN=J. Smith', 'L=London', 'C=England');
157        $split = Horde_Ldap_Util::splitRDNMultivalue($rdn);
158        $this->assertEquals($expected, $split);
159
160        // Unescaped "+" in value.
161        $rdn = 'OU=Sa+les+CN=J. Smith';
162        $expected = array('OU=Sa+les', 'CN=J. Smith');
163        $split = Horde_Ldap_Util::splitRDNMultivalue($rdn);
164        $this->assertEquals($expected, $split);
165
166        // Unescaped "+" in attr name.
167        $rdn = 'O+U=Sales+CN=J. Smith';
168        $expected = array('O+U=Sales', 'CN=J. Smith');
169        $split = Horde_Ldap_Util::splitRDNMultivalue($rdn);
170        $this->assertEquals($expected, $split);
171
172        // Unescaped "+" in attr name + value.
173        $rdn = 'O+U=Sales+CN=J. Sm+ith';
174        $expected = array('O+U=Sales', 'CN=J. Sm+ith');
175        $split = Horde_Ldap_Util::splitRDNMultivalue($rdn);
176        $this->assertEquals($expected, $split);
177
178        // Unescaped "+" in attribute name, but not first attribute.  This
179        // documents a known bug. However, unfortunately we can't know wether
180        // the "C+" belongs to value "Sales" or attribute "C+N".  To solve
181        // this, we must ask the schema which we do not right now.  The problem
182        // is located in _correct_dn_splitting().
183        $rdn = 'OU=Sales+C+N=J. Smith';
184        // The "C+" is treaten as value of "OU".
185        $expected = array('OU=Sales+C', 'N=J. Smith');
186        $split = Horde_Ldap_Util::splitRDNMultivalue($rdn);
187        $this->assertEquals($expected, $split);
188
189        // Escaped "+" in attribute name and value.
190        $rdn = 'O\+U=Sales+CN=J. Sm\+ith';
191        $expected = array('O\+U=Sales', 'CN=J. Sm\+ith');
192        $split = Horde_Ldap_Util::splitRDNMultivalue($rdn);
193        $this->assertEquals($expected, $split);
194    }
195
196    /**
197     * Tests attribute splitting ('foo=bar' => array('foo', 'bar'))
198     */
199    public function testSplitAttributeString()
200    {
201        $attr_str = 'foo=bar';
202
203        // Properly.
204        $expected = array('foo', 'bar');
205        $split = Horde_Ldap_Util::splitAttributeString($attr_str);
206        $this->assertEquals($expected, $split);
207
208        // Escaped "=".
209        $attr_str = "fo\=o=b\=ar";
210        $expected = array('fo\=o', 'b\=ar');
211        $split = Horde_Ldap_Util::splitAttributeString($attr_str);
212        $this->assertEquals($expected, $split);
213
214        // Escaped "=" and unescaped = later on.
215        $attr_str = "fo\=o=b=ar";
216        $expected = array('fo\=o', 'b=ar');
217        $split = Horde_Ldap_Util::splitAttributeString($attr_str);
218        $this->assertEquals($expected, $split);
219    }
220
221    /**
222     * Tests Ldap_explode_dn()
223     */
224    public function testExplodeDN()
225    {
226        $dn = 'ou=Sales+CN=J. Smith,dc=example,dc=net';
227        $expected_casefold_none = array(
228            array('CN=J. Smith', 'ou=Sales'),
229            'dc=example',
230            'dc=net'
231        );
232        $expected_casefold_upper = array(
233            array('CN=J. Smith', 'OU=Sales'),
234            'DC=example',
235            'DC=net'
236        );
237        $expected_casefold_lower = array(
238            array('cn=J. Smith', 'ou=Sales'),
239            'dc=example',
240            'dc=net'
241        );
242        $expected_onlyvalues = array(
243            array('J. Smith', 'Sales'),
244            'example',
245            'net'
246        );
247        $expected_reverse = array_reverse($expected_casefold_upper);
248
249
250        $dn_exploded_cnone = Horde_Ldap_Util::explodeDN($dn, array('casefold' => 'none'));
251        $this->assertEquals($expected_casefold_none, $dn_exploded_cnone, 'Option casefold none failed');
252
253        $dn_exploded_cupper = Horde_Ldap_Util::explodeDN($dn, array('casefold' => 'upper'));
254        $this->assertEquals($expected_casefold_upper, $dn_exploded_cupper, 'Option casefold upper failed');
255
256        $dn_exploded_clower = Horde_Ldap_Util::explodeDN($dn, array('casefold' => 'lower'));
257        $this->assertEquals($expected_casefold_lower, $dn_exploded_clower, 'Option casefold lower failed');
258
259        $dn_exploded_onlyval = Horde_Ldap_Util::explodeDN($dn, array('onlyvalues' => true));
260        $this->assertEquals($expected_onlyvalues, $dn_exploded_onlyval, 'Option onlyval failed');
261
262        $dn_exploded_reverse = Horde_Ldap_Util::explodeDN($dn, array('reverse' => true));
263        $this->assertEquals($expected_reverse, $dn_exploded_reverse, 'Option reverse failed');
264
265        $this->assertEquals(
266            array('CN=J\\, Smith', 'DC=example', 'DC=net'),
267            Horde_Ldap_Util::explodeDN('cn=J\\, Smith,dc=example,dc=net'));
268    }
269
270    /**
271     * Tests if canonicalDN() works.
272     *
273     * Note: This tests depend on the default options of canonicalDN().
274     */
275    public function testCanonicalDN()
276    {
277        // Test empty dn (is valid according to RFC).
278        $this->assertEquals('', Horde_Ldap_Util::canonicalDN(''));
279
280        // Default options with common DN.
281        $testdn   = 'cn=beni,DC=php,c=net';
282        $expected = 'CN=beni,DC=php,C=net';
283        $this->assertEquals($expected, Horde_Ldap_Util::canonicalDN($testdn));
284
285        // Casefold tests with common DN.
286        $expected_up = 'CN=beni,DC=php,C=net';
287        $expected_lo = 'cn=beni,dc=php,c=net';
288        $expected_no = 'cn=beni,DC=php,c=net';
289        $this->assertEquals($expected_up, Horde_Ldap_Util::canonicalDN($testdn, array('casefold' => 'upper')));
290        $this->assertEquals($expected_lo, Horde_Ldap_Util::canonicalDN($testdn, array('casefold' => 'lower')));
291        $this->assertEquals($expected_no, Horde_Ldap_Util::canonicalDN($testdn, array('casefold' => 'none')));
292
293        // Reverse.
294        $expected_rev = 'C=net,DC=php,CN=beni';
295        $this->assertEquals($expected_rev, Horde_Ldap_Util::canonicalDN($testdn, array('reverse' => true)), 'Option reverse failed');
296
297        // DN as arrays.
298        $dn_index = array('cn=beni', 'dc=php', 'c=net');
299        $dn_assoc = array('cn' => 'beni', 'dc' => 'php', 'c' => 'net');
300        $expected = 'CN=beni,DC=php,C=net';
301        $this->assertEquals($expected, Horde_Ldap_Util::canonicalDN($dn_index));
302        $this->assertEquals($expected, Horde_Ldap_Util::canonicalDN($dn_assoc));
303
304        // DN with multiple RDN value.
305        $testdn       = 'ou=dev+cn=beni,DC=php,c=net';
306        $testdn_index = array(array('ou=dev', 'cn=beni'), 'DC=php', 'c=net');
307        $testdn_assoc = array(array('ou' => 'dev', 'cn' => 'beni'), 'DC' => 'php', 'c' => 'net');
308        $expected     = 'CN=beni+OU=dev,DC=php,C=net';
309        $this->assertEquals($expected, Horde_Ldap_Util::canonicalDN($testdn));
310        $this->assertEquals($expected, Horde_Ldap_Util::canonicalDN($testdn_assoc));
311        $this->assertEquals($expected, Horde_Ldap_Util::canonicalDN($expected));
312
313        // Test DN with OID.
314        $testdn = 'OID.2.5.4.3=beni,dc=php,c=net';
315        $expected = '2.5.4.3=beni,DC=php,C=net';
316        $this->assertEquals($expected, Horde_Ldap_Util::canonicalDN($testdn));
317
318        // Test with leading and ending spaces.
319        $testdn   = 'cn=  beni  ,DC=php,c=net';
320        $expected = 'CN=\20\20beni\20\20,DC=php,C=net';
321        $this->assertEquals($expected, Horde_Ldap_Util::canonicalDN($testdn));
322
323        // Test with escaped commas. Doesn't work at the moment because
324        // canonicalDN() escapes attribute values, which break if they are
325        // already escaped.
326        $testdn   = 'cn=beni\\,hi\=ll,DC=php,c=net';
327        $expected = 'CN=beni\\,hi\=ll,DC=php,C=net';
328        // $this->assertEquals($expected, Horde_Ldap_Util::canonicalDN($testdn));
329
330        // Test with to-be escaped characters in attribute value.
331        $specialchars = array(
332            ',' => '\,',
333            '+' => '\+',
334            '"' => '\"',
335            '\\' => '\\\\',
336            '<' => '\<',
337            '>' => '\>',
338            ';' => '\;',
339            '#' => '\#',
340            '=' => '\=',
341            chr(18) => '\12',
342            '/' => '\/'
343        );
344        foreach ($specialchars as $char => $escape) {
345            $test_string = 'CN=be' . $char . 'ni,DC=ph' . $char . 'p,C=net';
346            $test_index  = array('CN=be' . $char . 'ni', 'DC=ph' . $char . 'p', 'C=net');
347            $test_assoc  = array('CN' => 'be' . $char . 'ni', 'DC' => 'ph' . $char . 'p', 'C' => 'net');
348            $expected    = 'CN=be' . $escape . 'ni,DC=ph' . $escape . 'p,C=net';
349
350            $this->assertEquals($expected, Horde_Ldap_Util::canonicalDN($test_string), 'String escaping test (' . $char . ') failed');
351            $this->assertEquals($expected, Horde_Ldap_Util::canonicalDN($test_index),  'Indexed array escaping test (' . $char . ') failed');
352            $this->assertEquals($expected, Horde_Ldap_Util::canonicalDN($test_assoc),  'Associative array encoding test (' . $char . ') failed');
353        }
354    }
355}
356