1/* LDAPSource.m - this file is part of SOGo
2 *
3 * Copyright (C) 2007-2015 Inverse inc.
4 *
5 * This file is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2, or (at your option)
8 * any later version.
9 *
10 * This file is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; see the file COPYING.  If not, write to
17 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
18 * Boston, MA 02111-1307, USA.
19 */
20
21#include <ldap.h>
22
23#import <Foundation/NSArray.h>
24#import <Foundation/NSDictionary.h>
25#import <Foundation/NSException.h>
26#import <Foundation/NSString.h>
27
28#import <NGExtensions/NSObject+Logs.h>
29#import <EOControl/EOControl.h>
30#import <NGLdap/NGLdapConnection.h>
31#import <NGLdap/NGLdapAttribute.h>
32#import <NGLdap/NGLdapEntry.h>
33#import <NGLdap/NGLdapModification.h>
34#import <NGLdap/NSString+DN.h>
35
36#import "LDAPSourceSchema.h"
37#import "NSArray+Utilities.h"
38#import "NSString+Utilities.h"
39#import "NSString+Crypto.h"
40#import "SOGoCache.h"
41#import "SOGoDomainDefaults.h"
42#import "SOGoSystemDefaults.h"
43
44#import "LDAPSource.h"
45#import "../../Main/SOGo.h"
46
47static Class NSStringK;
48
49#define SafeLDAPCriteria(x) [[[x stringByReplacingString: @"\\" withString: @"\\\\"] \
50                                 stringByReplacingString: @"'" withString: @"\\'"] \
51                                 stringByReplacingString: @"%" withString: @"%%"]
52
53@implementation LDAPSource
54
55+ (void) initialize
56{
57  NSStringK = [NSString class];
58}
59
60//
61//
62//
63+ (id) sourceFromUDSource: (NSDictionary *) udSource
64                 inDomain: (NSString *) sourceDomain
65{
66  id newSource;
67
68  newSource = [[self alloc] initFromUDSource: udSource
69                                    inDomain: sourceDomain];
70  [newSource autorelease];
71
72  return newSource;
73}
74
75//
76//
77//
78- (id) init
79{
80  if ((self = [super init]))
81    {
82      sourceID = nil;
83      displayName = nil;
84
85      bindDN = nil;
86      password = nil;
87      sourceBindDN = nil;
88      sourceBindPassword = nil;
89      hostname = nil;
90      port = 389;
91      encryption = nil;
92      domain = nil;
93
94      baseDN = nil;
95      schema = nil;
96      IDField = @"cn"; /* the first part of a user DN */
97      CNField = @"cn";
98      UIDField = @"uid";
99      mailFields = [NSArray arrayWithObject: @"mail"];
100      [mailFields retain];
101      contactMapping = nil;
102      searchFields = [NSArray arrayWithObjects: @"sn", @"displayname", @"telephonenumber", nil];
103      [searchFields retain];
104      groupObjectClasses = [NSArray arrayWithObjects: @"group", @"groupofnames", @"groupofuniquenames", @"posixgroup", nil];
105      [groupObjectClasses retain];
106      IMAPHostField = nil;
107      IMAPLoginField = nil;
108      SieveHostField = nil;
109      bindFields = nil;
110      _scope = @"sub";
111      _filter = nil;
112      _userPasswordAlgorithm = nil;
113      listRequiresDot = YES;
114
115      searchAttributes = nil;
116      passwordPolicy = NO;
117      updateSambaNTLMPasswords = NO;
118
119      kindField = nil;
120      multipleBookingsField = nil;
121
122      MSExchangeHostname = nil;
123
124      modifiers = nil;
125    }
126
127  return self;
128}
129
130//
131//
132//
133- (void) dealloc
134{
135  [schema release];
136  [bindDN release];
137  [password release];
138  [sourceBindDN release];
139  [sourceBindPassword release];
140  [hostname release];
141  [encryption release];
142  [baseDN release];
143  [IDField release];
144  [CNField release];
145  [UIDField release];
146  [contactMapping release];
147  [mailFields release];
148  [searchFields release];
149  [groupObjectClasses release];
150  [IMAPHostField release];
151  [IMAPLoginField release];
152  [SieveHostField release];
153  [bindFields release];
154  [_filter release];
155  [_userPasswordAlgorithm release];
156  [sourceID release];
157  [modulesConstraints release];
158  [_scope release];
159  [searchAttributes release];
160  [domain release];
161  [kindField release];
162  [multipleBookingsField release];
163  [MSExchangeHostname release];
164  [modifiers release];
165  [displayName release];
166  [super dealloc];
167}
168
169//
170//
171//
172- (id) initFromUDSource: (NSDictionary *) udSource
173               inDomain: (NSString *) sourceDomain
174{
175  SOGoDomainDefaults *dd;
176  NSNumber *udQueryLimit, *udQueryTimeout, *dotValue;
177
178  if ((self = [self init]))
179    {
180      [self setSourceID: [udSource objectForKey: @"id"]];
181      [self setDisplayName: [udSource objectForKey: @"displayName"]];
182
183      [self setBindDN: [udSource objectForKey: @"bindDN"]
184             password: [udSource objectForKey: @"bindPassword"]
185             hostname: [udSource objectForKey: @"hostname"]
186                 port: [udSource objectForKey: @"port"]
187           encryption: [udSource objectForKey: @"encryption"]
188    bindAsCurrentUser: [udSource objectForKey: @"bindAsCurrentUser"]];
189
190      [self setBaseDN: [udSource objectForKey: @"baseDN"]
191              IDField: [udSource objectForKey: @"IDFieldName"]
192              CNField: [udSource objectForKey: @"CNFieldName"]
193             UIDField: [udSource objectForKey: @"UIDFieldName"]
194           mailFields: [udSource objectForKey: @"MailFieldNames"]
195         searchFields: [udSource objectForKey: @"SearchFieldNames"]
196   groupObjectClasses: [udSource objectForKey: @"GroupObjectClasses"]
197        IMAPHostField: [udSource objectForKey: @"IMAPHostFieldName"]
198       IMAPLoginField: [udSource objectForKey: @"IMAPLoginFieldName"]
199       SieveHostField: [udSource objectForKey: @"SieveHostFieldName"]
200           bindFields: [udSource objectForKey: @"bindFields"]
201            kindField: [udSource objectForKey: @"KindFieldName"]
202            andMultipleBookingsField: [udSource objectForKey: @"MultipleBookingsFieldName"]];
203
204      dotValue = [udSource objectForKey: @"listRequiresDot"];
205      if (dotValue)
206        [self setListRequiresDot: [dotValue boolValue]];
207      [self setContactMapping: [udSource objectForKey: @"mapping"]
208             andObjectClasses: [udSource objectForKey: @"objectClasses"]];
209
210      [self setModifiers: [udSource objectForKey: @"modifiers"]];
211      ASSIGN (abOU, [udSource objectForKey: @"abOU"]);
212
213      if ([sourceDomain length])
214        {
215          dd = [SOGoDomainDefaults defaultsForDomain: sourceDomain];
216          ASSIGN (domain, sourceDomain);
217        }
218      else
219        dd = [SOGoSystemDefaults sharedSystemDefaults];
220
221      contactInfoAttribute
222        = [udSource objectForKey: @"SOGoLDAPContactInfoAttribute"];
223      if (!contactInfoAttribute)
224        contactInfoAttribute = [dd ldapContactInfoAttribute];
225      [contactInfoAttribute retain];
226
227      udQueryLimit = [udSource objectForKey: @"SOGoLDAPQueryLimit"];
228      if (udQueryLimit)
229        queryLimit = [udQueryLimit intValue];
230      else
231        queryLimit = [dd ldapQueryLimit];
232
233      udQueryTimeout = [udSource objectForKey: @"SOGoLDAPQueryTimeout"];
234      if (udQueryTimeout)
235        queryTimeout = [udQueryTimeout intValue];
236      else
237        queryTimeout = [dd ldapQueryTimeout];
238
239      ASSIGN(modulesConstraints,
240          [udSource objectForKey: @"ModulesConstraints"]);
241      ASSIGN(_filter, [udSource objectForKey: @"filter"]);
242      ASSIGN(_userPasswordAlgorithm, [udSource objectForKey: @"userPasswordAlgorithm"]);
243      ASSIGN(_scope, ([udSource objectForKey: @"scope"]
244                       ? [udSource objectForKey: @"scope"]
245                       : (id)@"sub"));
246
247      if (!_userPasswordAlgorithm)
248        _userPasswordAlgorithm = @"none";
249
250      if ([udSource objectForKey: @"passwordPolicy"])
251        passwordPolicy = [[udSource objectForKey: @"passwordPolicy"] boolValue];
252
253      if ([udSource objectForKey: @"updateSambaNTLMPasswords"])
254        updateSambaNTLMPasswords = [[udSource objectForKey: @"updateSambaNTLMPasswords"] boolValue];
255
256      ASSIGN(MSExchangeHostname, [udSource objectForKey: @"MSExchangeHostname"]);
257    }
258
259  return self;
260}
261
262- (void) setBindDN: (NSString *) theDN
263{
264  //NSLog(@"Setting bind DN to %@", theDN);
265  ASSIGN(bindDN, theDN);
266}
267
268- (NSString *) bindDN
269{
270  return bindDN;
271}
272
273- (void) setBindPassword: (NSString *) thePassword
274{
275  ASSIGN (password, thePassword);
276}
277
278- (NSString *) bindPassword
279{
280  return password;
281}
282
283- (BOOL) bindAsCurrentUser
284{
285  return _bindAsCurrentUser;
286}
287
288- (void) setBindDN: (NSString *) newBindDN
289          password: (NSString *) newBindPassword
290          hostname: (NSString *) newBindHostname
291              port: (NSString *) newBindPort
292        encryption: (NSString *) newEncryption
293 bindAsCurrentUser: (NSString *) bindAsCurrentUser
294{
295  ASSIGN(bindDN, newBindDN);
296  ASSIGN(password, newBindPassword);
297  ASSIGN(sourceBindDN, newBindDN);
298  ASSIGN(sourceBindPassword, newBindPassword);
299
300  ASSIGN(encryption, [newEncryption uppercaseString]);
301  if ([encryption isEqualToString: @"SSL"])
302    port = 636;
303  ASSIGN(hostname, newBindHostname);
304  if (newBindPort)
305    port = [newBindPort intValue];
306  _bindAsCurrentUser = [bindAsCurrentUser boolValue];
307}
308
309//
310//
311//
312- (void) setBaseDN: (NSString *) newBaseDN
313           IDField: (NSString *) newIDField
314           CNField: (NSString *) newCNField
315          UIDField: (NSString *) newUIDField
316        mailFields: (NSArray *) newMailFields
317      searchFields: (NSArray *) newSearchFields
318groupObjectClasses: (NSArray *) newGroupObjectClasses
319     IMAPHostField: (NSString *) newIMAPHostField
320    IMAPLoginField: (NSString *) newIMAPLoginField
321    SieveHostField: (NSString *) newSieveHostField
322        bindFields: (id) newBindFields
323         kindField: (NSString *) newKindField
324  andMultipleBookingsField: (NSString *) newMultipleBookingsField
325{
326  ASSIGN(baseDN, [newBaseDN lowercaseString]);
327  if (newIDField)
328    ASSIGN(IDField, [newIDField lowercaseString]);
329  if (newCNField)
330    ASSIGN(CNField, [newCNField lowercaseString]);
331  if (newUIDField)
332    ASSIGN(UIDField, [newUIDField lowercaseString]);
333  if (newIMAPHostField)
334    ASSIGN(IMAPHostField, [newIMAPHostField lowercaseString]);
335  if (newIMAPLoginField)
336    ASSIGN(IMAPLoginField, [newIMAPLoginField lowercaseString]);
337  if (newSieveHostField)
338    ASSIGN(SieveHostField, [newSieveHostField lowercaseString]);
339  if (newMailFields)
340    ASSIGN(mailFields, newMailFields);
341  if (newSearchFields)
342    ASSIGN(searchFields, newSearchFields);
343  if (newGroupObjectClasses)
344    ASSIGN(groupObjectClasses, newGroupObjectClasses);
345  if (newBindFields)
346    {
347      // Before SOGo v1.2.0, bindFields was a comma-separated list
348      // of values. So it could be configured as:
349      //
350      // bindFields = foo;
351      // bindFields = "foo, bar, baz";
352      //
353      // SOGo v1.2.0 and upwards redefined that parameter as an array
354      // so we would have instead:
355      //
356      // bindFields = (foo);
357      // bindFields = (foo, bar, baz);
358      //
359      // We check for the old format and we support it.
360      if ([newBindFields isKindOfClass: [NSArray class]])
361        ASSIGN(bindFields, newBindFields);
362      else
363        {
364          [self logWithFormat: @"WARNING: using old bindFields format - please update it"];
365          ASSIGN(bindFields, [newBindFields componentsSeparatedByString: @","]);
366        }
367    }
368  if (newKindField)
369    ASSIGN(kindField, [newKindField lowercaseString]);
370  if (newMultipleBookingsField)
371    ASSIGN(multipleBookingsField, [newMultipleBookingsField lowercaseString]);
372}
373
374- (void) setListRequiresDot: (BOOL) aBool
375{
376  listRequiresDot = aBool;
377}
378
379- (BOOL) listRequiresDot
380{
381  return listRequiresDot;
382}
383
384- (void) setContactMapping: (NSDictionary *) newMapping
385          andObjectClasses: (NSArray *) newObjectClasses
386{
387  ASSIGN (contactMapping, newMapping);
388  ASSIGN (contactObjectClasses, newObjectClasses);
389}
390
391//
392//
393//
394- (BOOL) _setupEncryption: (NGLdapConnection *) encryptedConn
395{
396  BOOL rc;
397
398  if ([encryption isEqualToString: @"SSL"])
399    rc = [encryptedConn useSSL];
400  else if ([encryption isEqualToString: @"STARTTLS"])
401    rc = [encryptedConn startTLS];
402  else
403    {
404      [self errorWithFormat:
405        @"encryption scheme '%@' not supported:"
406        @" use 'SSL' or 'STARTTLS'", encryption];
407      rc = NO;
408    }
409
410  return rc;
411}
412
413//
414//
415//
416- (NGLdapConnection *) _ldapConnection
417{
418  NGLdapConnection *ldapConnection;
419
420  NS_DURING
421    {
422      //NSLog(@"Creating NGLdapConnection instance for bindDN '%@'", bindDN);
423
424      ldapConnection = [[NGLdapConnection alloc] initWithHostName: hostname
425                                                             port: port];
426      [ldapConnection autorelease];
427      if (![encryption length] || [self _setupEncryption: ldapConnection])
428        {
429          [ldapConnection bindWithMethod: @"simple"
430                                  binddn: bindDN
431                             credentials: password];
432          if (queryLimit > 0)
433            [ldapConnection setQuerySizeLimit: queryLimit];
434          if (queryTimeout > 0)
435            [ldapConnection setQueryTimeLimit: queryTimeout];
436          if (!schema)
437            {
438              schema = [LDAPSourceSchema new];
439              [schema readSchemaFromConnection: ldapConnection];
440            }
441        }
442      else
443        ldapConnection = nil;
444    }
445  NS_HANDLER
446    {
447      [self errorWithFormat: @"Could not bind to the LDAP server %@ (%d) "
448                             @"using the bind DN: %@", hostname, port, bindDN];
449      [self errorWithFormat: @"%@", localException];
450      ldapConnection = nil;
451    }
452  NS_ENDHANDLER;
453
454  return ldapConnection;
455}
456
457- (NSString *) domain
458{
459  return domain;
460}
461
462/* user management */
463- (EOQualifier *) _qualifierForBindFilter: (NSString *) uid
464{
465  NSMutableString *qs;
466  NSString *escapedUid;
467  NSEnumerator *fields;
468  NSString *currentField;
469
470  qs = [NSMutableString string];
471
472  escapedUid = SafeLDAPCriteria(uid);
473
474  fields = [bindFields objectEnumerator];
475  while ((currentField = [fields nextObject]))
476    [qs appendFormat: @" OR (%@='%@')", currentField, escapedUid];
477
478  if (_filter && [_filter length])
479    [qs appendFormat: @" AND %@", _filter];
480
481  [qs deleteCharactersInRange: NSMakeRange(0, 4)];
482
483  return [EOQualifier qualifierWithQualifierFormat: qs];
484}
485
486- (NSString *) _fetchUserDNForLogin: (NSString *) loginToCheck
487{
488  NSEnumerator *entries;
489  EOQualifier *qualifier;
490  NSArray *attributes;
491  NGLdapConnection *ldapConnection;
492  NSString *userDN;
493
494  ldapConnection = [self _ldapConnection];
495  qualifier = [self _qualifierForBindFilter: loginToCheck];
496  attributes = [NSArray arrayWithObject: @"dn"];
497
498  if ([_scope caseInsensitiveCompare: @"BASE"] == NSOrderedSame)
499    entries = [ldapConnection baseSearchAtBaseDN: baseDN
500                                       qualifier: qualifier
501                                      attributes: attributes];
502  else if ([_scope caseInsensitiveCompare: @"ONE"] == NSOrderedSame)
503    entries = [ldapConnection flatSearchAtBaseDN: baseDN
504                                       qualifier: qualifier
505                                      attributes: attributes];
506  else
507    entries = [ldapConnection deepSearchAtBaseDN: baseDN
508                                       qualifier: qualifier
509                                      attributes: attributes];
510
511  userDN = [[entries nextObject] dn];
512
513  return userDN;
514}
515
516//
517//
518//
519- (BOOL) checkLogin: (NSString *) _login
520           password: (NSString *) _pwd
521               perr: (SOGoPasswordPolicyError *) _perr
522             expire: (int *) _expire
523              grace: (int *) _grace
524{
525  NGLdapConnection *bindConnection;
526  NSString *userDN;
527  BOOL didBind;
528
529  didBind = NO;
530
531  NS_DURING
532    if ([_login length] > 0 && [_pwd length] > 0)
533      {
534        bindConnection = [[NGLdapConnection alloc] initWithHostName: hostname
535                                                             port: port];
536        if (![encryption length] || [self _setupEncryption: bindConnection])
537          {
538            if (queryTimeout > 0)
539              [bindConnection setQueryTimeLimit: queryTimeout];
540
541            userDN = [[SOGoCache sharedCache] distinguishedNameForLogin: _login];
542
543            if (!userDN)
544              {
545                if (bindFields)
546                  {
547                    // We MUST always use the source's bindDN/password in
548                    // order to lookup the user's DN. This is important since
549                    // if we use bindAsCurrentUser, we could stay bound and
550                    // lookup the user's DN (for an other user that is trying
551                    // to log in) but not be able to do so due to ACLs in LDAP.
552                    [self setBindDN: sourceBindDN];
553                    [self setBindPassword: sourceBindPassword];
554                    userDN = [self _fetchUserDNForLogin: _login];
555                  }
556                else
557                  userDN = [NSString stringWithFormat: @"%@=%@,%@",
558                                   IDField, [_login escapedForLDAPDN], baseDN];
559              }
560
561            if (userDN)
562              {
563                if (!passwordPolicy)
564                  didBind = [bindConnection bindWithMethod: @"simple"
565                                                    binddn: userDN
566                                               credentials: _pwd];
567                else
568                  didBind = [bindConnection bindWithMethod: @"simple"
569                                                    binddn: userDN
570                                               credentials: _pwd
571                                                      perr: (void *)_perr
572                                                    expire: _expire
573                                                     grace: _grace];
574
575                if (didBind)
576                  // We cache the _login <-> userDN entry to speed up things
577                  [[SOGoCache sharedCache] setDistinguishedName: userDN
578                                                       forLogin: _login];
579              }
580          }
581      }
582  NS_HANDLER
583    {
584      [self logWithFormat: @"%@", localException];
585    }
586  NS_ENDHANDLER;
587
588  [bindConnection release];
589  return didBind;
590}
591
592/**
593 * Encrypts a string using this source password algorithm.
594 * @param plainPassword the unencrypted password.
595 * @return a new encrypted string.
596 * @see _isPassword:equalTo:
597 */
598- (NSString *) _encryptPassword: (NSString *) plainPassword
599{
600  NSString *pass;
601  pass = [plainPassword asCryptedPassUsingScheme: _userPasswordAlgorithm];
602
603  if (pass == nil)
604    {
605      [self errorWithFormat: @"Unsupported user-password algorithm: %@", _userPasswordAlgorithm];
606      return nil;
607    }
608
609  if ([_userPasswordAlgorithm caseInsensitiveCompare: @"md5-crypt"] == NSOrderedSame ||
610      [_userPasswordAlgorithm caseInsensitiveCompare: @"sha256-crypt"] == NSOrderedSame ||
611      [_userPasswordAlgorithm caseInsensitiveCompare: @"sha512-crypt"] == NSOrderedSame)
612    {
613      _userPasswordAlgorithm = @"crypt";
614    }
615
616  return [NSString stringWithFormat: @"{%@}%@", _userPasswordAlgorithm, pass];
617}
618
619- (BOOL)  _ldapModifyAttribute: (NSString *) theAttribute
620                     withValue: (NSString *) theValue
621                        userDN: (NSString *) theUserDN
622                      password: (NSString *) theUserPassword
623                    connection: (NGLdapConnection *) bindConnection
624{
625  NGLdapModification *mod;
626  NGLdapAttribute *attr;
627  NSArray *changes;
628
629  BOOL didChange;
630
631  attr = [[NGLdapAttribute alloc] initWithAttributeName: theAttribute];
632  [attr addStringValue: theValue];
633
634  mod = [NGLdapModification replaceModification: attr];
635
636  changes = [NSArray arrayWithObject: mod];
637
638  if ([bindConnection bindWithMethod: @"simple"
639                              binddn: theUserDN
640                         credentials: theUserPassword])
641    {
642      didChange = [bindConnection modifyEntryWithDN: theUserDN
643                                            changes: changes];
644    }
645  else
646    didChange = NO;
647
648  RELEASE(attr);
649
650  return didChange;
651}
652
653//
654//
655//
656- (BOOL) changePasswordForLogin: (NSString *) login
657                    oldPassword: (NSString *) oldPassword
658                    newPassword: (NSString *) newPassword
659                           perr: (SOGoPasswordPolicyError *) perr
660
661{
662  NGLdapConnection *bindConnection;
663  NSString *userDN;
664  BOOL didChange;
665
666  didChange = NO;
667
668  NS_DURING
669    if ([login length] > 0)
670      {
671        bindConnection = [[NGLdapConnection alloc] initWithHostName: hostname
672                                                             port: port];
673        if (![encryption length] || [self _setupEncryption: bindConnection])
674          {
675            if (queryTimeout > 0)
676              [bindConnection setQueryTimeLimit: queryTimeout];
677            if (bindFields)
678              userDN = [self _fetchUserDNForLogin: login];
679            else
680              userDN = [NSString stringWithFormat: @"%@=%@,%@",
681                     IDField, [login escapedForLDAPDN], baseDN];
682            if (userDN)
683              {
684                if ([bindConnection isADCompatible])
685                  {
686                    if ([bindConnection bindWithMethod: @"simple"
687                                                binddn: userDN
688                                           credentials: oldPassword])
689                      {
690                        didChange = [bindConnection changeADPasswordAtDn: userDN
691                                                             oldPassword: oldPassword
692                                                             newPassword: newPassword];
693                      }
694                  }
695                else if (passwordPolicy)
696                  {
697                    didChange = [bindConnection changePasswordAtDn: userDN
698                                                       oldPassword: oldPassword
699                                                       newPassword: newPassword
700                                                              perr: (void *)perr];
701                  }
702                else
703                  {
704                    // We don't use a password policy - we simply use
705                    // a modify-op to change the password
706                    NSString* encryptedPass;
707
708                    if ([_userPasswordAlgorithm isEqualToString: @"none"])
709                      {
710                        encryptedPass = newPassword;
711                      }
712                    else
713                      {
714                        encryptedPass = [self _encryptPassword: newPassword];
715                      }
716
717                    if (encryptedPass != nil)
718                      {
719                        *perr = PolicyNoError;
720                        didChange = [self _ldapModifyAttribute: @"userPassword"
721                                                     withValue: encryptedPass
722                                                        userDN: userDN
723                                                      password: oldPassword
724                                                    connection: bindConnection];
725                      }
726                  }
727
728                // We must check if we must update the Samba NT/LM password hashes
729                if (didChange && updateSambaNTLMPasswords)
730                  {
731                    [self _ldapModifyAttribute: @"sambaNTPassword"
732                                     withValue: [newPassword asNTHash]
733                                        userDN: userDN
734                                      password: newPassword
735                                    connection: bindConnection];
736
737                    [self _ldapModifyAttribute: @"sambaLMPassword"
738                                     withValue: [newPassword asLMHash]
739                                        userDN: userDN
740                                      password: newPassword
741                                    connection: bindConnection];
742                  }
743              }
744          }
745      }
746  NS_HANDLER
747    {
748      if ([[localException name] isEqual: @"LDAPException"] &&
749          ([[[localException userInfo] objectForKey: @"error_code"] intValue] == LDAP_CONSTRAINT_VIOLATION))
750        {
751          *perr = PolicyInsufficientPasswordQuality;
752        }
753      else
754        {
755          [self logWithFormat: @"%@", localException];
756        }
757    }
758  NS_ENDHANDLER ;
759
760  [bindConnection release];
761  return didChange;
762}
763
764
765/**
766 * Search for contacts matching some string.
767 * @param filter the string to search for
768 * @see fetchContactsMatching:
769 * @return a EOQualifier matching the filter
770 */
771- (EOQualifier *) _qualifierForFilter: (NSString *) filter
772{
773  NSMutableArray *fields;
774  NSString *fieldFormat, *searchFormat, *escapedFilter;
775  EOQualifier *qualifier;
776  NSMutableString *qs;
777
778  escapedFilter = SafeLDAPCriteria(filter);
779  if ([escapedFilter length] > 0)
780    {
781      qs = [NSMutableString string];
782      if ([escapedFilter isEqualToString: @"."])
783        [qs appendFormat: @"(%@='*')", CNField];
784      else
785        {
786          fieldFormat = [NSString stringWithFormat: @"(%%@='*%@*')", escapedFilter];
787          fields = [NSMutableArray arrayWithArray: searchFields];
788          [fields addObjectsFromArray: mailFields];
789          [fields addObject: CNField];
790          searchFormat = [[[fields uniqueObjects] stringsWithFormat: fieldFormat]
791            componentsJoinedByString: @" OR "];
792          [qs appendString: searchFormat];
793        }
794
795      if (_filter && [_filter length])
796        [qs appendFormat: @" AND %@", _filter];
797
798      qualifier = [EOQualifier qualifierWithQualifierFormat: qs];
799    }
800  else if (!listRequiresDot)
801    {
802      qs = [NSMutableString stringWithFormat: @"(%@='*')", CNField];
803      if ([_filter length])
804        [qs appendFormat: @" AND %@", _filter];
805      qualifier = [EOQualifier qualifierWithQualifierFormat: qs];
806    }
807  else
808    qualifier = nil;
809
810  return qualifier;
811}
812
813- (EOQualifier *) _qualifierForUIDFilter: (NSString *) uid
814{
815  NSString *mailFormat, *fieldFormat, *escapedUid, *currentField;
816  NSEnumerator *bindFieldsEnum;
817  NSMutableString *qs;
818
819  escapedUid = SafeLDAPCriteria(uid);
820
821  fieldFormat = [NSString stringWithFormat: @"(%%@='%@')", escapedUid];
822  mailFormat = [[mailFields stringsWithFormat: fieldFormat]
823                     componentsJoinedByString: @" OR "];
824  qs = [NSMutableString stringWithFormat: @"(%@='%@') OR %@",
825                        UIDField, escapedUid, mailFormat];
826  if (bindFields)
827    {
828      bindFieldsEnum = [bindFields objectEnumerator];
829      while ((currentField = [bindFieldsEnum nextObject]))
830        {
831          if ([currentField caseInsensitiveCompare: UIDField] != NSOrderedSame
832              && ![mailFields containsObject: currentField])
833            [qs appendFormat: @" OR (%@='%@')", [currentField stringByTrimmingSpaces], escapedUid];
834        }
835    }
836
837  if (_filter && [_filter length])
838    [qs appendFormat: @" AND %@", _filter];
839
840  return [EOQualifier qualifierWithQualifierFormat: qs];
841}
842
843- (NSArray *) _constraintsFields
844{
845  NSMutableArray *fields;
846  NSEnumerator *values;
847  NSDictionary *currentConstraint;
848
849  fields = [NSMutableArray array];
850  values = [[modulesConstraints allValues] objectEnumerator];
851  while ((currentConstraint = [values nextObject]))
852    [fields addObjectsFromArray: [currentConstraint allKeys]];
853
854  return fields;
855}
856
857/* This is required for SQL sources when DomainFieldName is enabled.
858 * For LDAP, simply discard the domain and call the original method */
859- (NSArray *) allEntryIDsVisibleFromDomain: (NSString *) domain
860{
861  return [self allEntryIDs];
862}
863
864- (NSArray *) allEntryIDs
865{
866  NSEnumerator *entries;
867  NGLdapEntry *currentEntry;
868  NGLdapConnection *ldapConnection;
869  EOQualifier *qualifier;
870  NSMutableString *qs;
871  NSString *value;
872  NSArray *attributes;
873  NSMutableArray *ids;
874
875  ids = [NSMutableArray array];
876
877  ldapConnection = [self _ldapConnection];
878  attributes = [NSArray arrayWithObject: IDField];
879
880  qs = [NSMutableString stringWithFormat: @"(%@='*')", CNField];
881  if ([_filter length])
882    [qs appendFormat: @" AND %@", _filter];
883  qualifier = [EOQualifier qualifierWithQualifierFormat: qs];
884
885  if ([_scope caseInsensitiveCompare: @"BASE"] == NSOrderedSame)
886    entries = [ldapConnection baseSearchAtBaseDN: baseDN
887                                       qualifier: qualifier
888                                      attributes: attributes];
889  else if ([_scope caseInsensitiveCompare: @"ONE"] == NSOrderedSame)
890    entries = [ldapConnection flatSearchAtBaseDN: baseDN
891                                       qualifier: qualifier
892                                      attributes: attributes];
893  else
894    entries = [ldapConnection deepSearchAtBaseDN: baseDN
895                                       qualifier: qualifier
896                                      attributes: attributes];
897
898  while ((currentEntry = [entries nextObject]))
899    {
900      value = [[currentEntry attributeWithName: IDField]
901                            stringValueAtIndex: 0];
902      if ([value length] > 0)
903        [ids addObject: value];
904    }
905
906  return ids;
907}
908
909- (void) _fillEmailsOfEntry: (NGLdapEntry *) ldapEntry
910             intoLDIFRecord: (NSMutableDictionary *) ldifRecord
911{
912  NSEnumerator *emailFields;
913  NSString *currentFieldName, *ldapValue;
914  NSMutableArray *emails;
915  NSArray *allValues;
916
917  emails = [[NSMutableArray alloc] init];
918  emailFields = [mailFields objectEnumerator];
919  while ((currentFieldName = [emailFields nextObject]))
920    {
921      allValues = [[ldapEntry attributeWithName: currentFieldName]
922                    allStringValues];
923
924      // Special case handling for Microsoft Active Directory. proxyAddresses
925      // is generally prefixed with smtp: - if we find this (or any value preceeding
926      // the semi-colon), we strip it. See https://msdn.microsoft.com/en-us/library/ms679424(v=vs.85).aspx
927      if ([currentFieldName caseInsensitiveCompare: @"proxyAddresses"] == NSOrderedSame)
928	{
929	  NSRange r;
930	  int i;
931
932	  for (i = 0; i < [allValues count]; i++)
933	    {
934	      ldapValue = [allValues objectAtIndex: i];
935	      r = [ldapValue rangeOfString: @":"];
936
937	      if (r.length)
938		{
939		  // We only keep "smtp" ones
940		  if ([[ldapValue lowercaseString] hasPrefix: @"smtp"])
941		    [emails addObject: [ldapValue substringFromIndex: r.location+1]];
942		}
943	      else
944		[emails addObject: ldapValue];
945	    }
946	}
947      else
948	[emails addObjectsFromArray: allValues];
949    }
950  [ldifRecord setObject: emails forKey: @"c_emails"];
951  [emails release];
952
953  if (IMAPHostField)
954    {
955      ldapValue = [[ldapEntry attributeWithName: IMAPHostField] stringValueAtIndex: 0];
956      if ([ldapValue length] > 0)
957        [ldifRecord setObject: ldapValue forKey: @"c_imaphostname"];
958    }
959
960  if (IMAPLoginField)
961    {
962      ldapValue = [[ldapEntry attributeWithName: IMAPLoginField] stringValueAtIndex: 0];
963      if ([ldapValue length] > 0)
964        [ldifRecord setObject: ldapValue forKey: @"c_imaplogin"];
965    }
966
967  if (SieveHostField)
968    {
969      ldapValue = [[ldapEntry attributeWithName: SieveHostField] stringValueAtIndex: 0];
970      if ([ldapValue length] > 0)
971        [ldifRecord setObject: ldapValue forKey: @"c_sievehostname"];
972    }
973}
974
975- (void) _fillConstraints: (NGLdapEntry *) ldapEntry
976                forModule: (NSString *) module
977           intoLDIFRecord: (NSMutableDictionary *) ldifRecord
978{
979  NSDictionary *constraints;
980  NSEnumerator *matches, *ldapValues;
981  NSString *currentMatch, *currentValue, *ldapValue;
982  BOOL result;
983
984  result = YES;
985
986  constraints = [modulesConstraints objectForKey: module];
987  if (constraints)
988    {
989      matches = [[constraints allKeys] objectEnumerator];
990      while (result == YES && (currentMatch = [matches nextObject]))
991        {
992          ldapValues = [[[ldapEntry attributeWithName: currentMatch] allStringValues] objectEnumerator];
993          currentValue = [constraints objectForKey: currentMatch];
994          result = NO;
995
996          while (result == NO && (ldapValue = [ldapValues nextObject]))
997            if ([ldapValue caseInsensitiveMatches: currentValue])
998              result = YES;
999        }
1000    }
1001
1002  [ldifRecord setObject: [NSNumber numberWithBool: result]
1003                 forKey: [NSString stringWithFormat: @"%@Access", module]];
1004}
1005
1006/* conversion LDAP -> SOGo inetOrgPerson entry */
1007- (void) applyContactMappingToResult: (NSMutableDictionary *) ldifRecord
1008{
1009  NSArray *sourceFields;
1010  NSArray *keys;
1011  NSString *key, *field, *value;
1012  NSUInteger count, max, fieldCount, fieldMax;
1013  BOOL filled;
1014
1015  keys = [contactMapping allKeys];
1016  max = [keys count];
1017  for (count = 0; count < max; count++)
1018    {
1019      key = [keys objectAtIndex: count];
1020      sourceFields = [contactMapping objectForKey: key];
1021      if ([sourceFields isKindOfClass: NSStringK])
1022        sourceFields = [NSArray arrayWithObject: sourceFields];
1023      fieldMax = [sourceFields count];
1024      filled = NO;
1025      for (fieldCount = 0;
1026           !filled && fieldCount < fieldMax;
1027           fieldCount++)
1028        {
1029          field = [[sourceFields objectAtIndex: fieldCount] lowercaseString];
1030          value = [ldifRecord objectForKey: field];
1031          if (value)
1032            {
1033              [ldifRecord setObject: value forKey: [key lowercaseString]];
1034              filled = YES;
1035            }
1036        }
1037    }
1038}
1039
1040/* conversion SOGo inetOrgPerson entry -> LDAP */
1041- (void) applyContactMappingToOutput: (NSMutableDictionary *) ldifRecord
1042{
1043  NSArray *sourceFields;
1044  NSArray *keys;
1045  NSString *key, *lowerKey, *field, *value;
1046  NSUInteger count, max, fieldCount, fieldMax;
1047
1048  if (contactObjectClasses)
1049    [ldifRecord setObject: contactObjectClasses
1050                   forKey: @"objectclass"];
1051
1052  keys = [contactMapping allKeys];
1053  max = [keys count];
1054  for (count = 0; count < max; count++)
1055    {
1056      key = [keys objectAtIndex: count];
1057      lowerKey = [key lowercaseString];
1058      value = [ldifRecord objectForKey: lowerKey];
1059      if ([value length] > 0)
1060        {
1061          sourceFields = [contactMapping objectForKey: key];
1062          if ([sourceFields isKindOfClass: NSStringK])
1063            sourceFields = [NSArray arrayWithObject: sourceFields];
1064
1065          fieldMax = [sourceFields count];
1066          for (fieldCount = 0; fieldCount < fieldMax; fieldCount++)
1067            {
1068              field = [[sourceFields objectAtIndex: fieldCount]
1069                        lowercaseString];
1070              [ldifRecord setObject: value forKey: field];
1071            }
1072        }
1073    }
1074}
1075
1076- (NSDictionary *) _convertLDAPEntryToContact: (NGLdapEntry *) ldapEntry
1077{
1078  NSMutableDictionary *ldifRecord;
1079  NSString *value;
1080  static NSArray *resourceKinds = nil;
1081  NSMutableArray *classes;
1082  NSEnumerator *gclasses;
1083  NSString *gclass;
1084  id o;
1085
1086  if (!resourceKinds)
1087    resourceKinds = [[NSArray alloc] initWithObjects: @"location", @"thing",
1088                                     @"group", nil];
1089
1090  ldifRecord = [ldapEntry asDictionary];
1091  [ldifRecord setObject: self forKey: @"source"];
1092  [ldifRecord setObject: [ldapEntry dn] forKey: @"dn"];
1093
1094  // We get our objectClass attribute values. We lowercase
1095  // everything for ease of search after.
1096  o = [ldapEntry objectClasses];
1097  classes = nil;
1098
1099  if (o)
1100    {
1101      int i, c;
1102
1103      classes = [NSMutableArray arrayWithArray: o];
1104      c = [classes count];
1105      for (i = 0; i < c; i++)
1106        [classes replaceObjectAtIndex: i
1107          withObject: [[classes objectAtIndex: i] lowercaseString]];
1108    }
1109
1110  if (classes)
1111    {
1112      // We check if our entry is a resource. We also support
1113      // determining resources based on the KindFieldName attribute
1114      // value - see below.
1115      if ([classes containsObject: @"calendarresource"])
1116        {
1117          [ldifRecord setObject: [NSNumber numberWithInt: 1]
1118                         forKey: @"isResource"];
1119        }
1120      else
1121        {
1122        // We check if our entry is a group. If so, we set the
1123        // 'isGroup' custom attribute.
1124        gclasses = [groupObjectClasses objectEnumerator];
1125        while ((gclass = [gclasses nextObject]))
1126         if ([classes containsObject: [gclass lowercaseString]])
1127           {
1128             [ldifRecord setObject: [NSNumber numberWithInt: 1]
1129                            forKey: @"isGroup"];
1130             break;
1131           }
1132        }
1133    }
1134
1135  // We check if that entry corresponds to a resource. For this,
1136  // kindField must be defined and it must hold one of those values
1137  //
1138  // location
1139  // thing
1140  // group
1141  //
1142  if ([kindField length] > 0)
1143    {
1144      value = [ldifRecord objectForKey: [kindField lowercaseString]];
1145      if ([value isKindOfClass: NSStringK]
1146          && [resourceKinds containsObject: value])
1147        [ldifRecord setObject: [NSNumber numberWithInt: 1]
1148                       forKey: @"isResource"];
1149    }
1150
1151  // We check for the number of simultanous bookings that is allowed.
1152  // A value of 0 means that there's no limit.
1153  if ([multipleBookingsField length] > 0)
1154    {
1155      value = [ldifRecord objectForKey: [multipleBookingsField lowercaseString]];
1156      [ldifRecord setObject: [NSNumber numberWithInt: [value intValue]]
1157                     forKey: @"numberOfSimultaneousBookings"];
1158    }
1159
1160  value = [[ldapEntry attributeWithName: IDField] stringValueAtIndex: 0];
1161  if (!value)
1162    value = @"";
1163  [ldifRecord setObject: value forKey: @"c_name"];
1164  value = [[ldapEntry attributeWithName: UIDField] stringValueAtIndex: 0];
1165  if (!value)
1166    value = @"";
1167//  else
1168//    {
1169//      Eventually, we could check at this point if the entry is a group
1170//      and prefix the UID with a "@"
1171//    }
1172  [ldifRecord setObject: value forKey: @"c_uid"];
1173  value = [[ldapEntry attributeWithName: CNField] stringValueAtIndex: 0];
1174  if (!value)
1175    value = @"";
1176  [ldifRecord setObject: value forKey: @"c_cn"];
1177  /* if "displayName" is not set, we use CNField because it must exist */
1178  if (![ldifRecord objectForKey: @"displayname"])
1179    [ldifRecord setObject: value forKey: @"displayname"];
1180
1181  if (contactInfoAttribute)
1182    {
1183      value = [[ldapEntry attributeWithName: contactInfoAttribute]
1184                stringValueAtIndex: 0];
1185      if (!value)
1186        value = @"";
1187    }
1188  else
1189    value = @"";
1190  [ldifRecord setObject: value forKey: @"c_info"];
1191
1192  if (domain)
1193    value = domain;
1194  else
1195    value = @"";
1196  [ldifRecord setObject: value forKey: @"c_domain"];
1197
1198  [self _fillEmailsOfEntry: ldapEntry intoLDIFRecord: ldifRecord];
1199  [self _fillConstraints: ldapEntry forModule: @"Calendar"
1200          intoLDIFRecord: (NSMutableDictionary *) ldifRecord];
1201  [self _fillConstraints: ldapEntry forModule: @"Mail"
1202          intoLDIFRecord: (NSMutableDictionary *) ldifRecord];
1203  [self _fillConstraints: ldapEntry forModule: @"ActiveSync"
1204          intoLDIFRecord: (NSMutableDictionary *) ldifRecord];
1205
1206  if (contactMapping)
1207    [self applyContactMappingToResult: ldifRecord];
1208
1209  return ldifRecord;
1210}
1211
1212- (NSArray *) fetchContactsMatching: (NSString *) match
1213                           inDomain: (NSString *) domain
1214{
1215  NGLdapConnection *ldapConnection;
1216  NGLdapEntry *currentEntry;
1217  NSEnumerator *entries;
1218  NSMutableArray *contacts;
1219  EOQualifier *qualifier;
1220  NSArray *attributes;
1221
1222  contacts = [NSMutableArray array];
1223
1224  if ([match length] > 0 || !listRequiresDot)
1225    {
1226      ldapConnection = [self _ldapConnection];
1227      qualifier = [self _qualifierForFilter: match];
1228      // attributes = [self _searchAttributes];
1229      attributes = [NSArray arrayWithObject: @"*"];
1230
1231      if ([_scope caseInsensitiveCompare: @"BASE"] == NSOrderedSame)
1232        entries = [ldapConnection baseSearchAtBaseDN: baseDN
1233                                           qualifier: qualifier
1234                                          attributes: attributes];
1235      else if ([_scope caseInsensitiveCompare: @"ONE"] == NSOrderedSame)
1236        entries = [ldapConnection flatSearchAtBaseDN: baseDN
1237                                           qualifier: qualifier
1238                                          attributes: attributes];
1239      else /* we do it like before */
1240        entries = [ldapConnection deepSearchAtBaseDN: baseDN
1241                                           qualifier: qualifier
1242                                          attributes: attributes];
1243      while ((currentEntry = [entries nextObject]))
1244        [contacts addObject:
1245                    [self _convertLDAPEntryToContact: currentEntry]];
1246    }
1247
1248  return contacts;
1249}
1250
1251- (NGLdapEntry *) _lookupLDAPEntry: (EOQualifier *) qualifier
1252{
1253  NGLdapConnection *ldapConnection;
1254  NSArray *attributes;
1255  NSEnumerator *entries;
1256
1257  // attributes = [self _searchAttributes];
1258  ldapConnection = [self _ldapConnection];
1259  attributes = [NSArray arrayWithObject: @"*"];
1260
1261  if ([_scope caseInsensitiveCompare: @"BASE"] == NSOrderedSame)
1262    entries = [ldapConnection baseSearchAtBaseDN: baseDN
1263                                       qualifier: qualifier
1264                                      attributes: attributes];
1265  else if ([_scope caseInsensitiveCompare: @"ONE"] == NSOrderedSame)
1266    entries = [ldapConnection flatSearchAtBaseDN: baseDN
1267                                       qualifier: qualifier
1268                                      attributes: attributes];
1269  else
1270    entries = [ldapConnection deepSearchAtBaseDN: baseDN
1271                                       qualifier: qualifier
1272                                      attributes: attributes];
1273
1274  return [entries nextObject];
1275}
1276
1277- (NSDictionary *) lookupContactEntry: (NSString *) theID
1278                             inDomain: (NSString *) domain
1279{
1280  NGLdapEntry *ldapEntry;
1281  EOQualifier *qualifier;
1282  NSString *s;
1283  NSDictionary *ldifRecord;
1284
1285  ldifRecord = nil;
1286
1287  if ([theID length] > 0)
1288    {
1289      s = [NSString stringWithFormat: @"(%@='%@')",
1290                    IDField, SafeLDAPCriteria(theID)];
1291      qualifier = [EOQualifier qualifierWithQualifierFormat: s];
1292      ldapEntry = [self _lookupLDAPEntry: qualifier];
1293      if (ldapEntry)
1294        ldifRecord = [self _convertLDAPEntryToContact: ldapEntry];
1295    }
1296
1297  return ldifRecord;
1298}
1299
1300- (NSDictionary *) lookupContactEntryWithUIDorEmail: (NSString *) uid
1301                                           inDomain: (NSString *) domain
1302{
1303  NGLdapEntry *ldapEntry;
1304  EOQualifier *qualifier;
1305  NSDictionary *ldifRecord;
1306
1307  ldifRecord = nil;
1308
1309  if ([uid length] > 0)
1310    {
1311      qualifier = [self _qualifierForUIDFilter: uid];
1312      ldapEntry = [self _lookupLDAPEntry: qualifier];
1313      if (ldapEntry)
1314        ldifRecord = [self _convertLDAPEntryToContact: ldapEntry];
1315    }
1316
1317  return ldifRecord;
1318}
1319
1320- (NSString *) lookupLoginByDN: (NSString *) theDN
1321{
1322  NGLdapConnection *ldapConnection;
1323  NGLdapEntry *entry;
1324  EOQualifier *qualifier;
1325  NSString *login;
1326
1327  login = nil;
1328  qualifier = nil;
1329
1330  ldapConnection = [self _ldapConnection];
1331
1332  if (_filter)
1333    qualifier = [EOQualifier qualifierWithQualifierFormat: _filter];
1334
1335  entry = [ldapConnection entryAtDN: theDN
1336			  qualifier: qualifier
1337                         attributes: [NSArray arrayWithObject: UIDField]];
1338  if (entry)
1339    login = [[entry attributeWithName: UIDField] stringValueAtIndex: 0];
1340
1341  return login;
1342}
1343
1344- (NSString *) lookupDNByLogin: (NSString *) theLogin
1345{
1346  return [[SOGoCache sharedCache] distinguishedNameForLogin: theLogin];
1347}
1348
1349- (NGLdapEntry *) _lookupGroupEntryByAttributes: (NSArray *) theAttributes
1350                                       andValue: (NSString *) theValue
1351{
1352  EOQualifier *qualifier;
1353  NGLdapEntry *ldapEntry;
1354  NSString *s;
1355
1356  if ([theValue length] > 0 && [theAttributes count] > 0)
1357    {
1358      if ([theAttributes count] == 1)
1359	{
1360	    s = [NSString stringWithFormat: @"(%@='%@')",
1361			  [theAttributes lastObject], SafeLDAPCriteria(theValue)];
1362
1363	}
1364      else
1365	{
1366	  NSString *fieldFormat;
1367
1368	  fieldFormat = [NSString stringWithFormat: @"(%%@='%@')", SafeLDAPCriteria(theValue)];
1369	  s = [[theAttributes stringsWithFormat: fieldFormat]
1370			 componentsJoinedByString: @" OR "];
1371	}
1372
1373      qualifier = [EOQualifier qualifierWithQualifierFormat: s];
1374      ldapEntry = [self _lookupLDAPEntry: qualifier];
1375    }
1376  else
1377    ldapEntry = nil;
1378
1379  return ldapEntry;
1380}
1381
1382- (NGLdapEntry *) lookupGroupEntryByUID: (NSString *) theUID
1383                               inDomain: (NSString *) domain
1384{
1385  return [self _lookupGroupEntryByAttributes: [NSArray arrayWithObject: UIDField]
1386				    andValue: theUID];
1387}
1388
1389- (NGLdapEntry *) lookupGroupEntryByEmail: (NSString *) theEmail
1390                                 inDomain: (NSString *) domain
1391{
1392  return [self _lookupGroupEntryByAttributes: mailFields
1393				    andValue: theEmail];
1394}
1395
1396- (void) setSourceID: (NSString *) newSourceID
1397{
1398  ASSIGN (sourceID, newSourceID);
1399}
1400
1401- (NSString *) sourceID
1402{
1403  return sourceID;
1404}
1405
1406- (void) setDisplayName: (NSString *) newDisplayName
1407{
1408  ASSIGN (displayName, newDisplayName);
1409}
1410
1411- (NSString *) displayName
1412{
1413  return displayName;
1414}
1415
1416- (NSString *) baseDN
1417{
1418  return baseDN;
1419}
1420
1421- (NSString *) MSExchangeHostname
1422{
1423  return MSExchangeHostname;
1424}
1425
1426- (void) setModifiers: (NSArray *) newModifiers
1427{
1428  ASSIGN (modifiers, newModifiers);
1429}
1430
1431- (NSArray *) modifiers
1432{
1433  return modifiers;
1434}
1435
1436- (NSArray *) groupObjectClasses
1437{
1438  return groupObjectClasses;
1439}
1440
1441static NSArray *
1442_convertRecordToLDAPAttributes (LDAPSourceSchema *schema, NSDictionary *ldifRecord)
1443{
1444  /* convert resulting record to NGLdapEntry:
1445     - strip non-existing object classes
1446     - ignore fields with empty values
1447     - ignore extra fields
1448     - use correct case for LDAP attribute matching classes */
1449  NSMutableArray *validClasses, *validFields, *attributes;
1450  NGLdapAttribute *attribute;
1451  NSArray *classes, *fields, *values;
1452  NSString *objectClass, *field, *lowerField, *value;
1453  NSUInteger count, max, valueCount, valueMax;
1454
1455  classes = [ldifRecord objectForKey: @"objectclass"];
1456  if ([classes isKindOfClass: NSStringK])
1457    classes = [NSArray arrayWithObject: classes];
1458  max = [classes count];
1459  validClasses = [NSMutableArray array];
1460  validFields = [NSMutableArray array];
1461  for (count = 0; count < max; count++)
1462    {
1463      objectClass = [classes objectAtIndex: count];
1464      fields = [schema fieldsForClass: objectClass];
1465      if ([fields count] > 0)
1466        {
1467          [validClasses addObject: objectClass];
1468          [validFields addObjectsFromArray: fields];
1469        }
1470    }
1471  [validFields removeDoubles];
1472
1473  attributes = [NSMutableArray new];
1474  max = [validFields count];
1475  for (count = 0; count < max; count++)
1476    {
1477      attribute = nil;
1478      field = [validFields objectAtIndex: count];
1479      lowerField = [field lowercaseString];
1480      if (![lowerField isEqualToString: @"dn"])
1481        {
1482          if ([lowerField isEqualToString: @"objectclass"])
1483            values = validClasses;
1484          else
1485            {
1486              values = [ldifRecord objectForKey: lowerField];
1487              if ([values isKindOfClass: NSStringK])
1488                values = [NSArray arrayWithObject: values];
1489            }
1490          valueMax = [values count];
1491          for (valueCount = 0; valueCount < valueMax; valueCount++)
1492            {
1493              value = [values objectAtIndex: valueCount];
1494              if ([value length] > 0)
1495                {
1496                  if (!attribute)
1497                    {
1498                      attribute = [[NGLdapAttribute alloc]
1499                                    initWithAttributeName: field];
1500                      [attributes addObject: attribute];
1501                      [attribute release];
1502                    }
1503                  [attribute addStringValue: value];
1504                }
1505            }
1506        }
1507    }
1508
1509  return attributes;
1510}
1511
1512- (NSException *) addContactEntry: (NSDictionary *) roLdifRecord
1513                           withID: (NSString *) aId
1514{
1515  NSException *result;
1516  NGLdapEntry *newEntry;
1517  NSMutableDictionary *ldifRecord;
1518  NSArray *attributes;
1519  NSString *dn, *cnValue;
1520  NGLdapConnection *ldapConnection;
1521
1522  if ([aId length] > 0)
1523    {
1524      ldapConnection = [self _ldapConnection];
1525      ldifRecord = [roLdifRecord mutableCopy];
1526      [ldifRecord autorelease];
1527      [ldifRecord setObject: aId forKey: UIDField];
1528
1529      /* if CN is not set, we use aId because it must exist */
1530      if (![ldifRecord objectForKey: CNField])
1531        {
1532          cnValue = [ldifRecord objectForKey: @"displayname"];
1533          if ([cnValue length] == 0)
1534            cnValue = aId;
1535          [ldifRecord setObject: aId forKey: @"cn"];
1536        }
1537
1538      [self applyContactMappingToOutput: ldifRecord];
1539
1540      /* since the id might have changed due to the mapping above, we
1541         reload the record ID */
1542      aId = [ldifRecord objectForKey: UIDField];
1543      dn = [NSString stringWithFormat: @"%@=%@,%@", IDField,
1544                     [aId escapedForLDAPDN], baseDN];
1545      attributes = _convertRecordToLDAPAttributes (schema, ldifRecord);
1546
1547      newEntry = [[NGLdapEntry alloc] initWithDN: dn
1548                                      attributes: attributes];
1549      [newEntry autorelease];
1550      [attributes release];
1551      NS_DURING
1552        {
1553          [ldapConnection addEntry: newEntry];
1554          result = nil;
1555        }
1556      NS_HANDLER
1557        {
1558          result = localException;
1559          [result retain];
1560        }
1561      NS_ENDHANDLER;
1562      [result autorelease];
1563    }
1564  else
1565    [self errorWithFormat: @"no value for id field '%@'", IDField];
1566
1567  return result;
1568}
1569
1570static NSArray *
1571_makeLDAPChanges (NGLdapConnection *ldapConnection,
1572                  NSString *dn, NSArray *attributes)
1573{
1574  NSMutableArray *changes, *attributeNames, *origAttributeNames;
1575  NGLdapEntry *origEntry;
1576  // NSArray *values;
1577  NGLdapAttribute *attribute, *origAttribute;
1578  NSString *name;
1579  NSDictionary *origAttributes;
1580  NSUInteger count, max/* , valueCount, valueMax */;
1581  // BOOL allStrings;
1582
1583  /* additions and modifications */
1584  origEntry = [ldapConnection entryAtDN: dn
1585                             attributes: [NSArray arrayWithObject: @"*"]];
1586  origAttributes = [origEntry attributes];
1587
1588  max = [attributes count];
1589  changes = [NSMutableArray arrayWithCapacity: max];
1590  attributeNames = [NSMutableArray arrayWithCapacity: max];
1591  for (count = 0; count < max; count++)
1592    {
1593      attribute = [attributes objectAtIndex: count];
1594      name = [attribute attributeName];
1595      [attributeNames addObject: name];
1596      origAttribute = [origAttributes objectForKey: name];
1597      if (origAttribute)
1598        {
1599          if (![origAttribute isEqual: attribute])
1600            [changes
1601              addObject: [NGLdapModification replaceModification: attribute]];
1602        }
1603      else
1604        [changes addObject: [NGLdapModification addModification: attribute]];
1605    }
1606
1607  /* deletions */
1608  origAttributeNames = [[origAttributes allKeys] mutableCopy];
1609  [origAttributeNames autorelease];
1610  [origAttributeNames removeObjectsInArray: attributeNames];
1611  max = [origAttributeNames count];
1612  for (count = 0; count < max; count++)
1613    {
1614      name = [origAttributeNames objectAtIndex: count];
1615      origAttribute = [origAttributes objectForKey: name];
1616      /* the attribute must only have string values, otherwise it will anyway
1617         be missing from the new record */
1618      // allStrings = YES;
1619      // values = [origAttribute allValues];
1620      // valueMax = [values count];
1621      // for (valueCount = 0; allStrings && valueCount < valueMax; valueCount++)
1622      //   if (![[values objectAtIndex: valueCount] isKindOfClass: NSStringK])
1623      //     allStrings = NO;
1624      // if (allStrings)
1625      [changes
1626        addObject: [NGLdapModification deleteModification: origAttribute]];
1627    }
1628
1629  return changes;
1630}
1631
1632- (NSException *) updateContactEntry: (NSDictionary *) roLdifRecord
1633{
1634  NSException *result;
1635  NSString *dn;
1636  NSMutableDictionary *ldifRecord;
1637  NSArray *attributes, *changes;
1638  NGLdapConnection *ldapConnection;
1639
1640  dn = [roLdifRecord objectForKey: @"dn"];
1641  if ([dn length] > 0)
1642    {
1643      ldapConnection = [self _ldapConnection];
1644      ldifRecord = [roLdifRecord mutableCopy];
1645      [ldifRecord autorelease];
1646      [self applyContactMappingToOutput: ldifRecord];
1647      attributes = _convertRecordToLDAPAttributes (schema, ldifRecord);
1648
1649      changes = _makeLDAPChanges (ldapConnection, dn, attributes);
1650
1651      NS_DURING
1652        {
1653          [ldapConnection modifyEntryWithDN: dn
1654                                    changes: changes];
1655          result = nil;
1656        }
1657      NS_HANDLER
1658        {
1659          result = localException;
1660          [result retain];
1661        }
1662      NS_ENDHANDLER;
1663      [result autorelease];
1664    }
1665  else
1666    [self errorWithFormat: @"expected dn for modified record"];
1667
1668  return result;
1669}
1670
1671- (NSException *) removeContactEntryWithID: (NSString *) aId
1672{
1673  NSException *result;
1674  NGLdapConnection *ldapConnection;
1675  NSString *dn;
1676
1677  ldapConnection = [self _ldapConnection];
1678  dn = [NSString stringWithFormat: @"%@=%@,%@", IDField,
1679                 [aId escapedForLDAPDN], baseDN];
1680  NS_DURING
1681    {
1682      [ldapConnection removeEntryWithDN: dn];
1683      result = nil;
1684    }
1685  NS_HANDLER
1686    {
1687      result = localException;
1688      [result retain];
1689    }
1690  NS_ENDHANDLER;
1691
1692  [result autorelease];
1693
1694  return result;
1695}
1696
1697/* user addressbooks */
1698- (BOOL) hasUserAddressBooks
1699{
1700  return ([abOU length] > 0);
1701}
1702
1703- (NSArray *) addressBookSourcesForUser: (NSString *) user
1704{
1705  NSMutableArray *sources;
1706  NSString *abBaseDN;
1707  NGLdapConnection *ldapConnection;
1708  NSArray *attributes, *modifier;
1709  NSEnumerator *entries;
1710  NGLdapEntry *entry;
1711  NSMutableDictionary *entryRecord;
1712  NSDictionary *sourceRec;
1713  LDAPSource *ab;
1714
1715  if ([self hasUserAddressBooks])
1716    {
1717      /* list subentries */
1718      sources = [NSMutableArray array];
1719
1720      ldapConnection = [self _ldapConnection];
1721      abBaseDN = [NSString stringWithFormat: @"ou=%@,%@=%@,%@",
1722                           [abOU escapedForLDAPDN], IDField,
1723                           [user escapedForLDAPDN], baseDN];
1724
1725      /* test ou=addressbooks entry */
1726      attributes = [NSArray arrayWithObject: @"*"];
1727      entries = [ldapConnection baseSearchAtBaseDN: abBaseDN
1728                                         qualifier: nil
1729                                        attributes: attributes];
1730      entry = [entries nextObject];
1731      if (entry)
1732        {
1733          attributes = [NSArray arrayWithObjects: @"ou", @"description", nil];
1734          entries = [ldapConnection flatSearchAtBaseDN: abBaseDN
1735                                             qualifier: nil
1736                                            attributes: attributes];
1737          modifier = [NSArray arrayWithObject: user];
1738          while ((entry = [entries nextObject]))
1739            {
1740              sourceRec = [entry asDictionary];
1741              ab = [LDAPSource new];
1742              [ab setSourceID: [sourceRec objectForKey: @"ou"]];
1743              [ab setDisplayName: [sourceRec objectForKey: @"description"]];
1744              [ab setBindDN: bindDN
1745		   password: password
1746		   hostname: hostname
1747		       port: [NSString stringWithFormat: @"%d", port]
1748		 encryption: encryption
1749		  bindAsCurrentUser: [NSString stringWithFormat: @"%d", NO]];
1750              [ab setBaseDN: [entry dn]
1751		    IDField: @"cn"
1752		    CNField: @"displayName"
1753		   UIDField: @"cn"
1754		 mailFields: nil
1755		  searchFields: nil
1756		  groupObjectClasses: nil
1757		  IMAPHostField: nil
1758		  IMAPLoginField: nil
1759		  SieveHostField: nil
1760		 bindFields: nil
1761		  kindField: nil
1762		  andMultipleBookingsField: nil];
1763              [ab setListRequiresDot: NO];
1764              [ab setModifiers: modifier];
1765              [sources addObject: ab];
1766              [ab release];
1767            }
1768        }
1769      else
1770        {
1771          entryRecord = [NSMutableDictionary dictionary];
1772          [entryRecord setObject: @"organizationalUnit" forKey: @"objectclass"];
1773          [entryRecord setObject: @"addressbooks" forKey: @"ou"];
1774          attributes = _convertRecordToLDAPAttributes (schema, entryRecord);
1775          entry = [[NGLdapEntry alloc] initWithDN: abBaseDN
1776                                       attributes: attributes];
1777          [entry autorelease];
1778          [attributes release];
1779          NS_DURING
1780            {
1781              [ldapConnection addEntry: entry];
1782            }
1783          NS_HANDLER
1784            {
1785              [self errorWithFormat: @"failed to create ou=addressbooks"
1786                    @" entry for user"];
1787            }
1788          NS_ENDHANDLER;
1789        }
1790    }
1791  else
1792    sources = nil;
1793
1794  return sources;
1795}
1796
1797- (NSException *) addAddressBookSource: (NSString *) newId
1798                       withDisplayName: (NSString *) newDisplayName
1799                               forUser: (NSString *) user
1800{
1801  NSException *result;
1802  NSString *abDN;
1803  NGLdapConnection *ldapConnection;
1804  NSArray *attributes;
1805  NGLdapEntry *entry;
1806  NSMutableDictionary *entryRecord;
1807
1808  if ([self hasUserAddressBooks])
1809    {
1810      abDN = [NSString stringWithFormat: @"ou=%@,ou=%@,%@=%@,%@",
1811                       [newId escapedForLDAPDN], [abOU escapedForLDAPDN],
1812                       IDField, [user escapedForLDAPDN], baseDN];
1813      entryRecord = [NSMutableDictionary dictionary];
1814      [entryRecord setObject: @"organizationalUnit" forKey: @"objectclass"];
1815      [entryRecord setObject: newId forKey: @"ou"];
1816      if ([newDisplayName length] > 0)
1817        [entryRecord setObject: newDisplayName forKey: @"description"];
1818      ldapConnection = [self _ldapConnection];
1819      attributes = _convertRecordToLDAPAttributes (schema, entryRecord);
1820      entry = [[NGLdapEntry alloc] initWithDN: abDN
1821                                   attributes: attributes];
1822      [entry autorelease];
1823      [attributes release];
1824      NS_DURING
1825        {
1826          [ldapConnection addEntry: entry];
1827          result = nil;
1828        }
1829      NS_HANDLER
1830        {
1831          [self errorWithFormat: @"failed to create addressbook entry"];
1832          result = localException;
1833          [result retain];
1834        }
1835      NS_ENDHANDLER;
1836      [result autorelease];
1837    }
1838  else
1839    result = [NSException exceptionWithName: @"LDAPSourceIOException"
1840                                     reason: @"user addressbooks"
1841                          @" are not supported"
1842                                   userInfo: nil];
1843
1844  return result;
1845}
1846
1847- (NSException *) renameAddressBookSource: (NSString *) newId
1848                          withDisplayName: (NSString *) newDisplayName
1849                                  forUser: (NSString *) user
1850{
1851  NSException *result;
1852  NSString *abDN;
1853  NGLdapConnection *ldapConnection;
1854  NSArray *attributes, *changes;
1855  NSMutableDictionary *entryRecord;
1856
1857  if ([self hasUserAddressBooks])
1858    {
1859      abDN = [NSString stringWithFormat: @"ou=%@,ou=%@,%@=%@,%@",
1860                       [newId escapedForLDAPDN], [abOU escapedForLDAPDN],
1861                       IDField, [user escapedForLDAPDN], baseDN];
1862      entryRecord = [NSMutableDictionary dictionary];
1863      [entryRecord setObject: @"organizationalUnit" forKey: @"objectclass"];
1864      [entryRecord setObject: newId forKey: @"ou"];
1865      if ([newDisplayName length] > 0)
1866        [entryRecord setObject: newDisplayName forKey: @"description"];
1867      ldapConnection = [self _ldapConnection];
1868      attributes = _convertRecordToLDAPAttributes (schema, entryRecord);
1869      changes = _makeLDAPChanges (ldapConnection, abDN, attributes);
1870      [attributes release];
1871      NS_DURING
1872        {
1873          [ldapConnection modifyEntryWithDN: abDN
1874                                    changes: changes];
1875          result = nil;
1876        }
1877      NS_HANDLER
1878        {
1879          [self errorWithFormat: @"failed to rename addressbook entry"];
1880          result = localException;
1881          [result retain];
1882        }
1883      NS_ENDHANDLER;
1884      [result autorelease];
1885    }
1886  else
1887    result = [NSException exceptionWithName: @"LDAPSourceIOException"
1888                                     reason: @"user addressbooks"
1889                          @" are not supported"
1890                                   userInfo: nil];
1891
1892  return result;
1893}
1894
1895- (NSException *) removeAddressBookSource: (NSString *) newId
1896                                  forUser: (NSString *) user
1897{
1898  NSException *result;
1899  NSString *abDN;
1900  NGLdapConnection *ldapConnection;
1901  NSEnumerator *entries;
1902  NGLdapEntry *entry;
1903
1904  if ([self hasUserAddressBooks])
1905    {
1906      abDN = [NSString stringWithFormat: @"ou=%@,ou=%@,%@=%@,%@",
1907                       [newId escapedForLDAPDN], [abOU escapedForLDAPDN],
1908                       IDField, [user escapedForLDAPDN], baseDN];
1909      ldapConnection = [self _ldapConnection];
1910      NS_DURING
1911        {
1912          /* we must remove the ab sub=entries prior to the ab entry */
1913          entries = [ldapConnection flatSearchAtBaseDN: abDN
1914                                             qualifier: nil
1915                                            attributes: nil];
1916          while ((entry = [entries nextObject]))
1917            [ldapConnection removeEntryWithDN: [entry dn]];
1918          [ldapConnection removeEntryWithDN: abDN];
1919          result = nil;
1920        }
1921      NS_HANDLER
1922        {
1923          [self errorWithFormat: @"failed to remove addressbook entry"];
1924          result = localException;
1925          [result retain];
1926        }
1927      NS_ENDHANDLER;
1928      [result autorelease];
1929    }
1930  else
1931    result = [NSException exceptionWithName: @"LDAPSourceIOException"
1932                                     reason: @"user addressbooks"
1933                          @" are not supported"
1934                                   userInfo: nil];
1935
1936  return result;
1937}
1938
1939@end
1940