1/*
2  Copyright (C) 2000-2005 SKYRIX Software AG
3
4  This file is part of SOPE.
5
6  SOPE is free software; you can redistribute it and/or modify it under
7  the terms of the GNU Lesser General Public License as published by the
8  Free Software Foundation; either version 2, or (at your option) any
9  later version.
10
11  SOPE is distributed in the hope that it will be useful, but WITHOUT ANY
12  WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
14  License for more details.
15
16  You should have received a copy of the GNU Lesser General Public
17  License along with SOPE; see the file COPYING.  If not, write to the
18  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
19  02111-1307, USA.
20*/
21
22#include "NGLdapFileManager.h"
23#include "NGLdapConnection.h"
24#include "NGLdapEntry.h"
25#include "NGLdapAttribute.h"
26#include "NGLdapURL.h"
27#include "NSString+DN.h"
28#import <NGExtensions/NGFileFolderInfoDataSource.h>
29#include "common.h"
30
31@implementation NGLdapFileManager
32
33static NSString *LDAPObjectClassKey = @"objectclass";
34static NSArray  *objectClassAttrs = nil;
35static NSArray  *fileInfoAttrs    = nil;
36
37+ (void)_initCache {
38  if (objectClassAttrs == nil) {
39    objectClassAttrs =
40      [[NSArray alloc] initWithObjects:&LDAPObjectClassKey count:1];
41  }
42  if (fileInfoAttrs == nil) {
43    fileInfoAttrs =
44      [[NSArray alloc] initWithObjects:
45                         @"objectclass",
46                         @"createTimestamp",
47                         @"modifyTimestamp",
48                         @"creatorsName",
49                         @"modifiersName",
50                         nil];
51  }
52}
53
54- (id)initWithLdapConnection:(NGLdapConnection *)_con { // designated initializer
55  if (_con == nil) {
56    [self release];
57    return nil;
58  }
59
60  [[self class] _initCache];
61
62  if ((self = [super init])) {
63    self->connection = [_con retain];
64  }
65  return self;
66}
67
68- (id)initWithHostName:(NSString *)_host port:(int)_port
69  bindDN:(NSString *)_login credentials:(NSString *)_pwd
70  rootDN:(NSString *)_rootDN
71{
72  NGLdapConnection *ldap;
73
74  ldap = [[NGLdapConnection alloc] initWithHostName:_host port:_port?_port:389];
75  if (ldap == nil) {
76    [self release];
77    return nil;
78  }
79  ldap = [ldap autorelease];
80
81  if (![ldap bindWithMethod:@"simple" binddn:_login credentials:_pwd]) {
82    NSLog(@"couldn't bind as DN '%@' with %@", _login, ldap);
83    [self release];
84    return nil;
85  }
86
87  if ((self = [self initWithLdapConnection:ldap])) {
88    if (_rootDN == nil) {
89      /* check cn=config as available in OpenLDAP */
90      NSArray *nctxs;
91
92      if ((nctxs = [self->connection namingContexts])) {
93        if ([nctxs count] > 1)
94          NSLog(@"WARNING: more than one naming context handled by server !");
95        if ([nctxs isNotEmpty])
96          _rootDN = [[nctxs objectAtIndex:0] lowercaseString];
97      }
98    }
99
100    if (_rootDN) {
101      ASSIGNCOPY(self->rootDN,    _rootDN);
102      ASSIGNCOPY(self->currentDN, _rootDN);
103      self->currentPath = @"/";
104    }
105  }
106  return self;
107}
108
109- (id)initWithURLString:(NSString *)_url {
110  NGLdapURL *url;
111
112  if ((url = [NGLdapURL ldapURLWithString:_url]) == nil) {
113    /* couldn't parse URL */
114    [self release];
115    return nil;
116  }
117
118  return [self initWithHostName:[url hostName] port:[url port]
119               bindDN:nil credentials:nil
120               rootDN:[url baseDN]];
121}
122- (id)initWithURL:(id)_url {
123  return (![_url isKindOfClass:[NSURL class]])
124    ? [self initWithURLString:[_url stringValue]]
125    : [self initWithURLString:[_url absoluteString]];
126}
127
128- (void)dealloc {
129  [self->connection  release];
130  [self->rootDN      release];
131  [self->currentDN   release];
132  [self->currentPath release];
133  [super dealloc];
134}
135
136/* internals */
137
138- (NSString *)_rdnForPathComponent:(NSString *)_pathComponent {
139  return _pathComponent;
140}
141- (NSString *)_pathComponentForRDN:(NSString *)_rdn {
142  return _rdn;
143}
144
145- (NSString *)pathForDN:(NSString *)_dn {
146  NSEnumerator *dnComponents;
147  NSString     *path;
148  NSString     *rdn;
149
150  if (_dn == nil) return nil;
151  _dn = [_dn lowercaseString];
152
153  if (![_dn hasSuffix:self->rootDN]) {
154    /* DN is not rooted in this hierachy */
155    return nil;
156  }
157
158  /* cut of root */
159  _dn = [_dn substringToIndex:([_dn length] - [self->rootDN length])];
160
161  path = @"/";
162  dnComponents = [[_dn dnComponents] reverseObjectEnumerator];
163  while ((rdn = [dnComponents nextObject])) {
164    NSString *pathComponent;
165
166    pathComponent = [self _pathComponentForRDN:rdn];
167
168    path = [path stringByAppendingPathComponent:pathComponent];
169  }
170  return path;
171}
172
173- (NGLdapConnection *)ldapConnection {
174  return self->connection;
175}
176- (NSString *)dnForPath:(NSString *)_path {
177  NSString *dn = nil;
178  NSArray  *pathComponents;
179  unsigned i, count;
180
181  if (![_path isAbsolutePath])
182    _path = [[self currentDirectoryPath] stringByAppendingPathComponent:_path];
183
184  if (![_path isNotEmpty]) return nil;
185
186  NSAssert1([_path isAbsolutePath],
187	    @"path %@ is not an absolute path (after append to cwd) !", _path);
188  NSAssert(self->rootDN, @"missing root DN !");
189
190  pathComponents = [_path pathComponents];
191  for (i = 0, count = [pathComponents count]; i < count; i++) {
192    NSString *pathComponent;
193    NSString *rdn;
194
195    pathComponent = [pathComponents objectAtIndex:i];
196
197    if ([pathComponent isEqualToString:@"."])
198      continue;
199    if (![pathComponent isNotEmpty])
200      continue;
201
202    if ([pathComponent isEqualToString:@"/"]) {
203      dn = self->rootDN;
204      continue;
205    }
206
207    if ([pathComponent isEqualToString:@".."]) {
208      dn = [dn stringByDeletingLastDNComponent];
209      continue;
210    }
211
212    rdn = [self _rdnForPathComponent:pathComponent];
213    dn  = [dn stringByAppendingDNComponent:rdn];
214  }
215
216  return [dn lowercaseString];
217}
218
219/* accessors */
220
221- (BOOL)changeCurrentDirectoryPath:(NSString *)_path {
222  NSString *dn;
223  NSString *path;
224
225  if (![_path isNotEmpty])
226    return NO;
227
228  if ((dn = [self dnForPath:_path]) == nil)
229    return NO;
230
231  if ((path = [self pathForDN:dn]) == nil)
232    return NO;
233
234  ASSIGNCOPY(self->currentDN,   dn);
235  ASSIGNCOPY(self->currentPath, path);
236  return YES;
237}
238
239- (NSString *)currentDirectoryPath {
240  return self->currentPath;
241}
242
243
244- (NSArray *)directoryContentsAtPath:(NSString *)_path {
245  NSString       *dn;
246  NSEnumerator   *e;
247  NSMutableArray *rdns;
248  NGLdapEntry    *entry;
249
250  if ((dn = [self dnForPath:_path]) == nil)
251    return nil;
252
253  e = [self->connection flatSearchAtBaseDN:dn
254                        qualifier:nil
255                        attributes:objectClassAttrs];
256  if (e == nil)
257    return nil;
258
259  rdns = nil;
260  while ((entry = [e nextObject])) {
261    if (rdns == nil)
262      rdns = [NSMutableArray arrayWithCapacity:128];
263
264    [rdns addObject:[entry rdn]];
265  }
266
267  return [[rdns copy] autorelease];
268}
269
270- (NSArray *)subpathsAtPath:(NSString *)_path {
271  NSString       *dn;
272  NSEnumerator   *e;
273  NSMutableArray *paths;
274  NGLdapEntry    *entry;
275
276  if ((dn = [self dnForPath:_path]) == nil)
277    return nil;
278
279  _path = [self pathForDN:dn];
280
281  e = [self->connection deepSearchAtBaseDN:dn
282                        qualifier:nil
283                        attributes:objectClassAttrs];
284  if (e == nil)
285    return nil;
286
287  paths = nil;
288  while ((entry = [e nextObject])) {
289    NSString *path;
290    NSString *sdn;
291
292    sdn = [entry dn];
293
294    if ((path = [self pathForDN:sdn]) == nil) {
295      NSLog(@"got no path for dn '%@' ..", sdn);
296      continue;
297    }
298
299    if ([path hasPrefix:_path])
300      path = [path substringFromIndex:[_path length]];
301
302    if (paths == nil)
303      paths = [NSMutableArray arrayWithCapacity:128];
304
305    [paths addObject:path];
306  }
307
308  return [[paths copy] autorelease];
309}
310
311- (NSDictionary *)fileAttributesAtPath:(NSString *)_path traverseLink:(BOOL)_fl {
312  NSString        *dn;
313  NGLdapEntry     *entry;
314  NGLdapAttribute *attr;
315  id    keys[10];
316  id    vals[10];
317  short count;
318
319  if ((dn = [self dnForPath:_path]) == nil)
320    return nil;
321
322  entry = [self->connection entryAtDN:dn attributes:fileInfoAttrs];
323  if (entry == nil)
324    return nil;
325
326  count = 0;
327  if ((attr = [entry attributeWithName:@"modifytimestamp"])) {
328    keys[count] = NSFileModificationDate;
329    vals[count] = [[attr stringValueAtIndex:0] ldapTimestamp];
330    count++;
331  }
332  if ((attr = [entry attributeWithName:@"modifiersname"])) {
333    keys[count] = NSFileOwnerAccountName;
334    vals[count] = [[attr allStringValues] componentsJoinedByString:@","];
335    count++;
336  }
337  if ((attr = [entry attributeWithName:@"creatorsname"])) {
338    keys[count] = @"NSFileCreatorAccountName";
339    vals[count] = [[attr allStringValues] componentsJoinedByString:@","];
340    count++;
341  }
342  if ((attr = [entry attributeWithName:@"createtimestamp"])) {
343    keys[count] = @"NSFileCreationDate";
344    vals[count] = [[attr stringValueAtIndex:0] ldapTimestamp];
345    count++;
346  }
347  if ((attr = [entry attributeWithName:@"objectclass"])) {
348    keys[count] = @"LDAPObjectClasses";
349    vals[count] = [attr allStringValues];
350    count++;
351  }
352
353  keys[count] = @"NSFileIdentifier";
354  if ((vals[count] = [entry dn]))
355    count++;
356
357  keys[count] = NSFilePath;
358  if ((vals[count] = _path))
359    count++;
360
361  keys[count] = NSFileName;
362  if ((vals[count] = [self _pathComponentForRDN:[dn lastDNComponent]]))
363    count++;
364
365  return [NSDictionary dictionaryWithObjects:vals forKeys:keys count:count];
366}
367
368/* determine access */
369
370- (BOOL)fileExistsAtPath:(NSString *)_path {
371  return [self fileExistsAtPath:_path isDirectory:NULL];
372}
373- (BOOL)fileExistsAtPath:(NSString *)_path isDirectory:(BOOL *)_isDir {
374  NSString    *dn;
375  NGLdapEntry *entry;
376
377  if ((dn = [self dnForPath:_path]) == nil)
378    return NO;
379
380  entry = [self->connection entryAtDN:dn attributes:objectClassAttrs];
381  if (entry == nil)
382    return NO;
383
384  if (_isDir) {
385    NSEnumerator *e;
386
387    /* is-dir based on child-availablitiy */
388    e = [self->connection flatSearchAtBaseDN:dn
389                          qualifier:nil
390                          attributes:objectClassAttrs];
391    *_isDir = [e nextObject] ? YES : NO;
392  }
393  return YES;
394}
395
396- (BOOL)isReadableFileAtPath:(NSString *)_path {
397  return [self fileExistsAtPath:_path];
398}
399- (BOOL)isWritableFileAtPath:(NSString *)_path {
400  return [self fileExistsAtPath:_path];
401}
402- (BOOL)isExecutableFileAtPath:(NSString *)_path {
403  return NO;
404}
405- (BOOL)isDeletableFileAtPath:(NSString *)_path {
406  return [self fileExistsAtPath:_path];
407}
408
409/* reading contents */
410
411- (BOOL)contentsEqualAtPath:(NSString *)_path1 andPath:(NSString *)_path2 {
412  NSString    *dn1, *dn2;
413  NGLdapEntry *e1, *e2;
414
415  if ((dn1 = [self dnForPath:_path1]) == nil)
416    return NO;
417  if ((dn2 = [self dnForPath:_path2]) == nil)
418    return NO;
419
420  if ([dn1 isEqualToString:dn2])
421    /* same DN */
422    return YES;
423
424  e1 = [self->connection entryAtDN:dn1 attributes:nil];
425  e2 = [self->connection entryAtDN:dn2 attributes:nil];
426
427  return [e1 isEqual:e2];
428}
429- (NSData *)contentsAtPath:(NSString *)_path {
430  /* generate LDIF for record */
431  NSString        *dn;
432  NGLdapEntry     *entry;
433
434  if ((dn = [self dnForPath:_path]) == nil)
435    return nil;
436
437  entry = [self->connection entryAtDN:dn attributes:nil];
438  if (entry == nil)
439    return nil;
440
441  return [[entry ldif] dataUsingEncoding:NSUTF8StringEncoding];
442}
443
444/* modifications */
445
446- (NSDictionary *)_errDictForPath:(NSString *)_path toPath:(NSString *)_dest
447  dn:(NSString *)_dn reason:(NSString *)_reason
448{
449  id    keys[6];
450  id    values[6];
451  short count;
452
453  count = 0;
454
455  if (_path) {
456    keys[count]   = @"Path";
457    values[count] = _path;
458    count++;
459  }
460  if (_dest) {
461    keys[count]   = @"ToPath";
462    values[count] = _dest;
463    count++;
464  }
465  if (_reason) {
466    keys[count]   = @"Error";
467    values[count] = _reason;
468    count++;
469  }
470  if (_dn) {
471    keys[count]   = @"dn";
472    values[count] = _dn;
473    count++;
474    keys[count]   = @"ldap";
475    values[count] = self->connection;
476    count++;
477  }
478
479  return [NSDictionary dictionaryWithObjects:values forKeys:keys count:count];
480}
481
482- (BOOL)removeFileAtPath:(NSString *)_path handler:(id)_fhandler {
483  NSString *dn;
484
485  [_fhandler fileManager:(id)self willProcessPath:_path];
486
487  if ((dn = [self dnForPath:_path]) == nil) {
488    if (_fhandler) {
489      NSDictionary *errDict;
490
491      errDict = [self _errDictForPath:_path toPath:nil dn:nil
492                      reason:@"couldn't map path to LDAP dn"];
493
494      if ([_fhandler fileManager:(id)self shouldProceedAfterError:errDict])
495        return YES;
496    }
497    return NO;
498  }
499
500  /* should delete sub-entries first ... */
501
502  /* delete entry */
503
504  if (![self->connection removeEntryWithDN:dn]) {
505    if (_fhandler) {
506      NSDictionary *errDict;
507
508      errDict = [self _errDictForPath:_path toPath:nil dn:dn
509                      reason:@"couldn't remove LDAP entry"];
510
511      if ([_fhandler fileManager:(id)self shouldProceedAfterError:errDict])
512        return YES;
513    }
514    return NO;
515  }
516
517  return YES;
518}
519
520- (BOOL)copyPath:(NSString *)_path toPath:(NSString *)_destination
521  handler:(id)_fhandler
522{
523  NGLdapEntry *e;
524  NSString    *fromDN, *toDN, *toRDN;
525
526  [_fhandler fileManager:(id)self willProcessPath:_path];
527
528  if ((fromDN = [self dnForPath:_path]) == nil) {
529    if (_fhandler) {
530      NSDictionary *errDict;
531
532      errDict = [self _errDictForPath:_path toPath:_destination dn:nil
533                      reason:@"couldn't map source path to LDAP dn"];
534
535      if ([_fhandler fileManager:(id)self shouldProceedAfterError:errDict])
536        return YES;
537    }
538    return NO;
539  }
540
541  /*
542    split destination. 'toDN' is the target 'directory', 'toRDN' the name of
543    the target 'file'
544  */
545  toDN  = [self dnForPath:_destination];
546  toRDN = [toDN lastDNComponent];
547  toDN  = [toDN stringByDeletingLastDNComponent];
548
549  if ((toDN == nil) || (toRDN == nil)) {
550    if (_fhandler) {
551      NSDictionary *errDict;
552
553      errDict = [self _errDictForPath:_path toPath:_destination dn:fromDN
554                      reason:@"couldn't map destination path to LDAP dn"];
555
556      if ([_fhandler fileManager:(id)self shouldProceedAfterError:errDict])
557        return YES;
558    }
559    return NO;
560  }
561
562  /* process record */
563
564  if ((e = [self->connection entryAtDN:fromDN attributes:nil]) == nil) {
565    if (_fhandler) {
566      NSDictionary *errDict;
567
568      errDict = [self _errDictForPath:_path toPath:_destination dn:fromDN
569                      reason:@"couldn't load source LDAP record"];
570
571      if ([_fhandler fileManager:(id)self shouldProceedAfterError:errDict])
572        return YES;
573    }
574    return NO;
575  }
576  else {
577    /* create new record with the attributes of the old one */
578    NGLdapEntry *newe;
579    NSArray     *attrs;
580
581    attrs = [[e attributes] allValues];
582    newe  = [[NGLdapEntry alloc] initWithDN:toDN attributes:attrs];
583    newe  = [newe autorelease];
584
585    /* insert record in target space */
586    if (![self->connection addEntry:newe]) {
587      /* insert failed */
588
589      if (_fhandler) {
590        NSDictionary *errDict;
591
592        errDict = [self _errDictForPath:_path toPath:_destination dn:toDN
593                        reason:@"couldn't insert LDAP record in target dn"];
594
595        if ([_fhandler fileManager:(id)self shouldProceedAfterError:errDict])
596          return YES;
597      }
598      return NO;
599    }
600  }
601
602  /* should process children ? */
603
604  return YES;
605}
606
607- (BOOL)movePath:(NSString *)_path toPath:(NSString *)_destination
608  handler:(id)_fhandler
609{
610  /* needs to invoke a modrdn operation */
611  [_fhandler fileManager:(id)self willProcessPath:_path];
612
613  return NO;
614}
615
616- (BOOL)linkPath:(NSString *)_path toPath:(NSString *)_destination
617  handler:(id)_fhandler
618{
619  /* LDAP doesn't support links .. */
620  [_fhandler fileManager:(id)self willProcessPath:_path];
621
622  return NO;
623}
624
625- (BOOL)createFileAtPath:(NSString *)path
626  contents:(NSData *)contents
627  attributes:(NSDictionary *)attributes
628{
629  return NO;
630}
631
632/* description */
633
634- (NSString *)description {
635  NSMutableString *ms;
636
637  ms = [NSMutableString stringWithCapacity:64];
638  [ms appendFormat:@"<0x%p[%@]:", self, NSStringFromClass([self class])];
639
640  if (self->rootDN)
641    [ms appendFormat:@" root=%@", self->rootDN];
642  if (self->currentDN && ![self->currentDN isEqualToString:self->rootDN])
643    [ms appendFormat:@" cwd=%@", self->currentDN];
644
645  if (self->connection)
646    [ms appendFormat:@" ldap=%@", self->connection];
647
648  [ms appendString:@">"];
649  return ms;
650}
651
652@end /* NGLdapFileManager */
653
654#include <NGLdap/NGLdapDataSource.h>
655#include <NGLdap/NGLdapGlobalID.h>
656
657@implementation NGLdapFileManager(ExtendedFileManager)
658
659/* feature check */
660
661- (BOOL)supportsVersioningAtPath:(NSString *)_path {
662  return NO;
663}
664- (BOOL)supportsLockingAtPath:(NSString *)_path {
665  return NO;
666}
667- (BOOL)supportsFolderDataSourceAtPath:(NSString *)_path {
668  return YES;
669}
670
671/* writing */
672
673- (BOOL)writeContents:(NSData *)_content atPath:(NSString *)_path {
674  /* should decode LDIF and store at path .. */
675  return NO;
676}
677
678/* datasources (work on folders) */
679
680- (EODataSource *)dataSourceAtPath:(NSString *)_path {
681  NGLdapDataSource *ds;
682  NSString *dn;
683
684  if ((dn = [self dnForPath:_path]) == nil)
685    /* couldn't get DN for specified path .. */
686    return nil;
687
688  ds = [[NGLdapDataSource alloc]
689                          initWithLdapConnection:self->connection
690                          searchBase:dn];
691  return [ds autorelease];
692}
693- (EODataSource *)dataSource {
694  return [self dataSourceAtPath:[self currentDirectoryPath]];
695}
696
697/* global-IDs */
698
699- (EOGlobalID *)globalIDForPath:(NSString *)_path {
700  NSString       *dn;
701  NGLdapGlobalID *gid;
702
703  if ((dn = [self dnForPath:_path]) == nil)
704    return nil;
705
706  gid = [[NGLdapGlobalID alloc]
707                         initWithHost:[self->connection hostName]
708                         port:[self->connection port]
709                         dn:dn];
710  return [gid autorelease];
711}
712
713- (NSString *)pathForGlobalID:(EOGlobalID *)_gid {
714  NGLdapGlobalID *gid;
715
716  if (![_gid isKindOfClass:[NGLdapGlobalID class]])
717    return nil;
718
719  gid = (NGLdapGlobalID *)_gid;
720
721  /* check whether host&port is correct */
722  if (![[self->connection hostName] isEqualToString:[gid host]])
723    return nil;
724  if (![self->connection port] == [gid port])
725    return nil;
726
727  return [self pathForDN:[gid dn]];
728}
729
730/* trash */
731
732- (BOOL)supportsTrashFolderAtPath:(NSString *)_path {
733  return NO;
734}
735- (NSString *)trashFolderForPath:(NSString *)_path {
736  return nil;
737}
738
739@end /* NGLdapFileManager(ExtendedFileManager) */
740