source: tol/tolsh/tolsh.vfs/lib/tcllib1.10/ldap/ldapx.tcl

devel
Last change on this file was 8597867d, checked in by Jorge Pérez <josp.jorge@…>, 16 years ago

closes #976, se incluye tcllib1.10, base64 debe eliminarse del raiz de lib ya que esta incluido en tcllib

git-svn-id: ​https://www.tol-project.org/svn/tolp/trunk@2556 be4846ae-f051-483a-831f-b64e7d08527f

  • Property mode set to 100644
File size: 36.9 KB
Line 
1#
2# Extended object interface to entries in LDAP directories or LDIF files.
3#
4# (c) 2006 Pierre David (pdav@users.sourceforge.net)
5#
6# $Id: ldapx.tcl,v 1.11 2007/08/19 20:20:43 pdav Exp $
7#
8# History:
9#   2006/08/08 : pda : design
10#
11
12package require Tcl 8.4
13package require snit            ;# tcllib
14package require uri 1.1.5       ;# tcllib
15package require base64          ;# tcllib
16package require ldap 1.6        ;# tcllib, low level code for LDAP directories
17
18package provide ldapx 1.0
19
20##############################################################################
21# LDAPENTRY object type
22##############################################################################
23
24snit::type ::ldapx::entry {
25    #########################################################################
26    # Variables
27    #########################################################################
28
29    #
30    # Format of an individual entry
31    # May be "standard" (standard LDAP entry, read from an LDAP directory
32    # or from a LDIF channel) or "change" (LDIF change, or result of the
33    # comparison of two standard entries).
34    # Special : "uninitialized" means that this entry has not been used,
35    # and the first use will initialize it.
36    #
37
38    variable format "uninitialized"
39
40    #
41    # DN
42    #
43
44    variable dn ""
45
46    #
47    # Standard entry
48    #
49    # Syntax:
50    #   - array indexed by attribute names (lower case)
51    #   - each value is the list of attributes
52    #
53    # The current state may be backed up in an internal state.
54    # (see backup and restore methods)
55    #
56
57    variable attrvals -array {}
58
59    variable backup 0
60    variable bckav  -array {}
61    variable bckdn  ""
62
63    #
64    # Change entry
65    #
66    # Syntax:
67    #   {{<op> <parameters>} ... }
68    #       if <op> = mod
69    #           {mod {{<modop> <attr> [ {<val1> ... <valn>} ]} ...} }
70    #           where <modop> = modrepl, modadd, moddel
71    #       if <op> = add
72    #           {add {<attr> {<val1> ... <valn>} ...}}
73    #       if <op> = del
74    #           {del}
75    #       if <op> = modrdn
76    #           {modrdn <newrdn> <deleteoldrdn> [ <newsuperior> ]}
77    #
78
79    variable change ""
80
81    #########################################################################
82    # Generic methods (for both standard and change entries)
83    #########################################################################
84
85    # Resets the entry to an empty state
86
87    method reset {} {
88
89        set format "uninitialized"
90        set dn ""
91        array unset attrvals
92        set backup 0
93        array unset bckav
94        set bckdn  ""
95        set change ""
96    }
97
98    # Returns current format
99
100    method format {} {
101
102        return $format
103    }
104
105    # Checks if entry is compatible with a certain format
106    # errors out if not
107
108    method compatible {ref} {
109
110        if {$format eq "uninitialized"} then {
111            set format $ref
112        } elseif {$format ne $ref} then {
113            return -code error \
114                "Invalid operation on format $format (should be $ref)"
115        }
116    }
117
118    # Get or set the current dn
119
120    method dn {{newdn {-}}} {
121
122        if {$newdn ne "-"} then {
123            set dn $newdn
124        }
125        return $dn
126    }
127
128    # Get the "superior" (LDAP slang word) part of current dn
129
130    method superior {} {
131
132        set pos [string first "," $dn]
133        if {$pos == -1} then {
134            set r ""
135        } else {
136            set r [string range $dn [expr {$pos+1}] end]
137        }
138        return $r
139    }
140
141    # Get the "rdn" part of current dn
142
143    method rdn {} {
144
145        set pos [string first "," $dn]
146        if {$pos == -1} then {
147            set r ""
148        } else {
149            set r [string range $dn 0 [expr {$pos-1}]]
150        }
151        return $r
152    }
153
154    # Get a printable form of the contents
155
156    method print {} {
157
158        set r "dn: $dn"
159        switch -- $format {
160            uninitialized {
161                # nothing
162            }
163            standard {
164                foreach a [lsort [array names attrvals]] {
165                    append r "\n$a: $attrvals($a)"
166                }
167            }
168            change {
169                if {[llength $change]} then {
170                    append r "\n$change"
171                }
172            }
173            default {
174                append r " (inconsistent value)"
175            }
176        }
177        return $r
178    }
179
180    # Prints the whole state of an entry
181
182    method debug {} {
183
184        set r "dn = <$dn>\nformat = $format"
185        switch -- $format {
186            uninitialized {
187                # nothing
188            }
189            standard {
190                foreach a [lsort [array names attrvals]] {
191                    append r "\n\t$a: $attrvals($a)"
192                }
193                if {$backup} then {
194                    append r "\nbackup dn = $bckdn"
195                    foreach a [lsort [array names bckav]] {
196                        append r "\n\t$a: $bckav($a)"
197                    }
198                } else {
199                    append r "\nno backup"
200                }
201            }
202            change {
203                if {[llength $change]} then {
204                    append r "\n$change"
205                } else {
206                    append r "\nno change"
207                }
208            }
209            default {
210                append r " (inconsistent value)"
211            }
212        }
213        return $r
214    }
215
216
217    #########################################################################
218    # Methods for standard entries
219    #########################################################################
220
221    # Tells if the current entry is empty
222
223    method isempty {} {
224
225        $self compatible "standard"
226
227        return [expr {[array size attrvals] == 0}]
228    }
229
230    # Get all values for an attribute
231
232    method get {attr} {
233
234        $self compatible "standard"
235
236        set a [string tolower $attr]
237        if {[info exists attrvals($a)]} then {
238            set r $attrvals($a)
239        } else {
240            set r {}
241        }
242        return $r
243    }
244
245    # Get only the first value for an attribute
246
247    method get1 {attr} {
248
249        return [lindex [$self get $attr] 0]
250    }
251
252
253    # Set all values for an attribute
254
255    method set {attr vals} {
256
257        $self compatible "standard"
258
259        set a [string tolower $attr]
260        if {[llength $vals]} then {
261            set attrvals($a) $vals
262        } else {
263            unset -nocomplain attrvals($a)
264        }
265        return $vals
266    }
267
268    # Set only one value for an attribute
269
270    method set1 {attr val} {
271
272        if {$val eq ""} then {
273            set l {}
274        } else {
275            set l [list $val]
276        }
277
278        return [$self set $attr $l]
279    }
280
281    # Add some values to an attribute
282
283    method add {attr vals} {
284
285        $self compatible "standard"
286
287        set a [string tolower $attr]
288        foreach v $vals {
289            lappend attrvals($a) $v
290        }
291        return $attrvals($a)
292    }
293
294    # Add only one value to an attribute
295
296    method add1 {attr val} {
297
298        return [$self add $attr [list $val]]
299    }
300
301    # Delete all values (or some values only) for an attribute
302
303    method del {attr {vals {}}} {
304
305        $self compatible "standard"
306
307        set a [string tolower $attr]
308        if {[llength $vals]} then {
309            set l [$self get $attr]
310            foreach v $vals {
311                while {[set pos [lsearch -exact $l $v]] != -1} {
312                    set l [lreplace $l $pos $pos]
313                }
314            }
315        } else {
316            set l {}
317        }
318
319        if {[llength $l]} then {
320            $self set $attr $l
321        } else {
322            unset -nocomplain attrvals($a)
323        }
324        return
325    }
326
327    # Delete only one value from an attribute
328
329    method del1 {attr val} {
330
331        $self del $attr [list $val]
332    }
333
334    # Get all attribute names
335
336    method getattr {} {
337
338        $self compatible "standard"
339
340        return [array names attrvals]
341    }
342
343    # Get all attribute names and values
344
345    method getall {} {
346
347        $self compatible "standard"
348
349        return [array get attrvals]
350    }
351
352    # Reset all attribute names and values at once
353
354    method setall {lst} {
355
356        $self compatible "standard"
357
358        array unset attrvals
359        foreach {attr vals} $lst {
360            set a [string tolower $attr]
361            set attrvals($a) $vals
362        }
363    }
364
365    # Back up current entry into a new one or into the internal backup state
366
367    method backup {{other {}}} {
368
369        $self compatible "standard"
370
371        if {$other eq ""} then {
372            #
373            # Back-up entry in $self->$oldav and $self->$dn
374            #
375            set backup 1
376            set bckdn $dn
377
378            array unset bckav
379            array set bckav [array get attrvals]
380        } else {
381            #
382            # Back-up entry in $other
383            #
384            $other compatible "standard"
385            $other dn $dn
386            $other setall [array get attrvals]
387        }
388    }
389
390    # Restore current entry from an old one or from the internal backup state
391
392    method restore {{other {}}} {
393
394        $self compatible "standard"
395
396        if {$backup} then {
397            if {$other eq ""} then {
398                #
399                # Restore in current context
400                #
401                set dn $bckdn
402                array unset attrvals
403                array set attrvals [array get bckav]
404            } else {
405                #
406                # Restore in another object
407                #
408                $other compatible "standard"
409                $other dn $bckdn
410                $other setall [array get bckav]
411            }
412        } else {
413            return -code error \
414                "Cannot restore a non backuped object"
415        }
416    }
417
418    # Swap current and backup data, if they reside in the same entry
419
420    method swap {} {
421
422        $self compatible "standard"
423
424        if {$backup} then {
425            #
426            # Swap current and backup contexts
427            #
428            set swdn $dn
429            set dn $bckdn
430            set bckdn $swdn
431
432            set swav [array get attrvals]
433            array unset attrvals
434            array set attrvals [array get bckav]
435            array unset bckav
436            array set bckav $swav
437        } else {
438            return -code error \
439                "Cannot swap a non backuped object"
440        }
441    }
442
443    # Apply some modifications (given by a change entry) to current entry
444
445    method apply {chg} {
446
447        $self compatible "standard"
448        $chg  compatible "change"
449
450        #
451        # Apply $chg modifications to $self
452        #
453
454        foreach mod [$chg change] {
455            set op [lindex $mod 0]
456            switch -- $op {
457                add {
458                    if {! [$self isempty]} then {
459                        return -code error \
460                            "Cannot add an entry to a non-empty entry"
461                    }
462                    $self setall [lindex $mod 1]
463                    if {[string equal [$self dn] ""]} then {
464                        $self dn [$chg dn]
465                    }
466                }
467                mod {
468                    foreach submod [lindex $mod 1] {
469                        set subop [lindex $submod 0]
470                        set attr [lindex $submod 1]
471                        set vals [lindex $submod 2]                 
472                        switch -- $subop {
473                            modadd {
474                                $self add $attr $vals
475                            }
476                            moddel {
477                                $self del $attr $vals
478                            }
479                            modrepl {
480                                $self del $attr
481                                $self add $attr $vals
482                            }
483                            default {
484                                return -code error \
485                                    "Invalid submod operation '$subop'"
486                            }
487                        }
488                    }
489                }
490                del {
491                    array unset attrvals
492                }
493                modrdn {
494                    set newrdn [lindex $mod 1]
495                    set delold [lindex $mod 2]
496                    set newsup [lindex $mod 3]
497
498                    if {! [regexp {^([^=]+)=([^,]+)$} $newrdn m nattr nval]} then {
499                        return -code "Invalid new RDN '$newrdn'"
500                    }
501
502                    set olddn  [$self dn]
503                    if {! [regexp {^([^=]+)=([^,]+),(.*)} $olddn m oattr oval osup]} then {
504                        return -code "Invalid old DN '$olddn'"
505                    }
506
507                    if {$newsup eq ""} then {
508                        set dn "$newrdn,$osup"
509                    } else {
510                        set dn "$newrdn,$newsup"
511                    }
512                    $self dn $dn
513
514                    if {$delold} then {
515                        $self del1 $oattr $oval
516                    }
517
518                    # XXX should we ignore case ?
519                    if {[lsearch -exact [$self get $nattr] $nval] == -1} then {
520                        $self add1 $nattr $nval
521                    }
522                }
523                default {
524                    return -code error \
525                        "Invalid change operation '$op'"
526                }
527            }
528        }
529    }
530
531    #########################################################################
532    # Methods for change entries
533    #########################################################################
534
535    # Get or set all modifications
536
537    method change {{newchg {-}}} {
538
539        $self compatible "change"
540
541        if {$newchg ne "-"} then {
542            set change $newchg
543        }
544        return $change
545    }
546
547    # Compute the difference between two entries (or between an entry
548    # and the backed-up internal state) into the current change entry
549    # e1 : new, e2 : old
550    # if e2 is not given, it defaults to backup in e1
551
552    method diff {new {old {}}} {
553
554        $self compatible "change"
555
556        #
557        # Select where backup is. If internal, creates a temporary
558        # standard entry.
559        #
560
561        if {$old eq ""} then {
562            set destroy_old 1
563            set old [::ldapx::entry create %AUTO%]
564            $new restore $old
565        } else {
566            set destroy_old 0
567        }
568
569        #
570        # Computes differences between values in the two entries
571        #
572
573        if {[$old dn] ne ""} then {
574            $self dn [$old dn]
575        } elseif {[$new dn] ne ""} then {
576            $self dn [$new dn]
577        } else {
578            $self dn ""
579        }
580
581        switch -- "[$new isempty][$old isempty]" {
582            00 {
583                # They may differ
584                set change [DiffEntries $new $old]
585            }
586            01 {
587                # new has been added
588                set change [list [list "add" [$new getall]]]
589            }
590            10 {
591                # new has been deleted
592                set change [list [list "del"]]
593            }
594            11 {
595                # they are both empty: no change
596                set change {}
597            }
598        }
599
600        #
601        # Remove temporary standard entry (backup was internal)
602        #
603
604        if {$destroy_old} then {
605            $old destroy
606        }
607
608        return $change
609    }
610
611    # local procedure to compute differences between two non empty entries
612
613    proc DiffEntries {new old} {
614        array set tnew [$new getall]
615        array set told [$old getall]
616
617        set lmod {}
618
619        #
620        # First step : is there a DN change?
621        #
622
623        set moddn [DiffDn [$new dn] [$old dn] tnew told]
624
625        #
626        # Second step : pick up changes in attributes and/or values
627        #
628
629        foreach a [array names tnew] {
630            if {[info exists told($a)]} then {
631                #
632                # They are new and old values for this attribute.
633                # We cannot use individual delete or add (rfc 4512,
634                # paragraph 2.5.1) for attributes which do not have an
635                # equality operator, so we use "replace" everywhere.
636                #
637
638                set lnew [lsort $tnew($a)]
639                set lold [lsort $told($a)]
640                if {$lold ne $lnew} then {
641                    lappend lmod [list "modrepl" $a $tnew($a)]
642                }
643
644                unset tnew($a)
645                unset told($a)
646            } else {
647                lappend lmod [list "modadd" $a $tnew($a)]
648                unset tnew($a)
649            }
650        }
651
652        foreach a [array names told] {
653            lappend lmod [list "moddel" $a]
654        }
655
656        set lchg {}
657
658        if {[llength $lmod]} then {
659            lappend lchg [list "mod" $lmod]
660        }
661
662        #
663        # Third step : insert modDN changes
664        #
665
666        if {[llength $moddn]} then {
667            set newrdn       [lindex $moddn 0]
668            set deleteoldrdn [lindex $moddn 1]
669            set newsuperior  [lindex $moddn 2]
670
671            set lmod [list "modrdn" $newrdn $deleteoldrdn]
672            if {! [string equal $newsuperior ""]} then {
673                lappend lmod $newsuperior
674            }
675            lappend lchg $lmod
676        }
677
678        return $lchg
679    }
680
681    proc DiffDn {newdn olddn _tnew _told} {
682        upvar $_tnew tnew
683        upvar $_told told
684
685        #
686        # If DNs are the same, exit
687        #
688
689        if {[string equal -nocase $newdn $olddn]} then {
690            return {}
691        }
692
693        #
694        # Split components of both DNs : attribute, value, superior
695        #
696
697        if {! [regexp {^([^=]+)=([^,]+),(.*)} $olddn m oattr oval osup]} then {
698            return -code "Invalid old DN '$olddn'"
699        }
700        set oattr [string tolower $oattr]
701        set ordn "$oattr=$oval"
702
703        if {! [regexp {^([^=]+)=([^,]+),(.*)} $newdn m nattr nval nsup]} then {
704            return -code "Invalid new DN '$newdn'"
705        }
706        set nattr [string tolower $nattr]
707        set nrdn "$nattr=$nval"
708
709        #
710        # Checks if superior has changed
711        #
712
713        if {! [string equal -nocase $osup $nsup]} then {
714            set newsuperior $nsup
715        } else {
716            set newsuperior ""
717        }
718
719        #
720        # Checks if rdn has changed
721        #
722
723        if {! [string equal -nocase $ordn $nrdn]} then {
724            #
725            # Checks if old rdn must be deleted
726            #
727
728            set deleteoldrdn 1
729            if {[info exists tnew($oattr)]} then {
730                set pos [lsearch -exact [string tolower $tnew($oattr)] \
731                                        [string tolower $oval]]
732                if {$pos != -1} then {
733                    set deleteoldrdn 0
734                }
735            }
736
737            #
738            # Remove old and new rdn such as DiffEntries doesn't
739            # detect any modification.
740            #
741
742            foreach t {tnew told} {
743                foreach {a v} [list $oattr $oval $nattr $nval] {
744                    if {[info exists ${t}($a)]} then {
745                        set l [set ${t}($a)]
746                        set pos [lsearch -exact [string tolower $l] \
747                                                [string tolower $v] ]
748                        if {$pos != -1} then {
749                            set l [lreplace $l $pos $pos]
750                            if {[llength $l]} then {
751                                set ${t}($a) $l
752                            } else {
753                                unset -nocomplain ${t}($a)
754                            }
755                        }
756                    }
757                }
758            }
759        } else {
760            set deleteoldrdn 0
761        }
762
763        return [list $nrdn $deleteoldrdn $newsuperior]
764    }
765
766
767    #########################################################################
768    # End of ldapentry
769    #########################################################################
770}
771
772##############################################################################
773# UTF8 translator, component used to manage the -utf8 option
774##############################################################################
775
776snit::type ::ldapx::utf8trans {
777
778    #########################################################################
779    # Option
780    #########################################################################
781
782    option -utf8         -default {{.*} {}}
783
784    #########################################################################
785    # Methods
786    #########################################################################
787
788    method must {attr} {
789        set utf8yes [lindex $options(-utf8) 0]
790        set utf8no  [lindex $options(-utf8) 1]
791        set r 0
792        if {[regexp -expanded -nocase "^$utf8yes$" $attr]} then {
793            set r 1
794            if {[regexp -expanded -nocase "^$utf8no$" $attr]} then {
795                set r 0
796            }
797        }
798        return $r
799    }
800
801    method encode {attr val} {
802        if {[$self must $attr]} then {
803            set val [encoding convertto utf-8 $val]
804        }
805        return $val
806    }
807
808    method decode {attr val} {
809        if {[$self must $attr]} then {
810            set val [encoding convertfrom utf-8 $val]
811        }
812        return $val
813    }
814
815    method encodepairs {avpairs} {
816        set r {}
817        foreach {attr vals} $avpairs {
818            if {[llength $vals]} then {
819                lappend r $attr [$self encode $attr $vals]
820            } else {
821                lappend r $attr
822            }
823        }
824        return $r
825    }
826
827    method decodepairs {avpairs} {
828        set r {}
829        foreach {attr vals} $avpairs {
830            set vals [$self decode $attr $vals]
831            lappend r $attr $vals
832        }
833        return $r
834    }
835}
836
837##############################################################################
838# LDAP object type
839##############################################################################
840
841snit::type ::ldapx::ldap {
842    #########################################################################
843    # Options
844    #
845    # note : options are lowercase
846    #########################################################################
847
848    option -scope        -default "sub"
849    option -derefaliases -default "never"
850    option -sizelimit    -default 0
851    option -timelimit    -default 0
852    option -attrsonly    -default 0
853
854    component translator
855    delegate option -utf8 to translator
856
857    #
858    # Channel descriptor
859    #
860
861    variable channel ""
862    variable bind 0
863
864    #
865    # Last error
866    #
867
868    variable lastError ""
869
870    #
871    # Defaults connection modes
872    #
873
874    variable connect_defaults -array {
875                                    ldap {389 ::ldap::connect}
876                                    ldaps {636 ::ldap::secure_connect}
877                                }
878
879
880    #########################################################################
881    # Constructor
882    #########################################################################
883
884    constructor {args} {
885        install translator using ::ldapx::utf8trans create %AUTO%
886        $self configurelist $args
887    }
888
889    destructor {
890        catch {$translator destroy}
891    }
892
893    #########################################################################
894    # Methods
895    #########################################################################
896
897    # Get or set the last error message
898
899    method error {{le {-}}} {
900
901        if {! [string equal $le "-"]} then {
902            set lastError $le
903        }
904        return $lastError
905    }
906
907    # Connect to the LDAP directory, and binds to it if needed
908
909    method connect {url {binddn {}} {bindpw {}}} {
910
911        array set comp [::uri::split $url "ldap"]
912
913        if {! [::info exists comp(host)]} then {
914            $self error "Invalid host in URL '$url'"
915            return 0
916        }
917
918        set scheme $comp(scheme)
919        if {! [::info exists connect_defaults($scheme)]} then {
920            $self error "Unrecognized URL '$url'"
921            return 0
922        }
923
924        set defport [lindex $connect_defaults($scheme) 0]
925        set fct     [lindex $connect_defaults($scheme) 1]
926
927        if {[string equal $comp(port) ""]} then {
928            set comp(port) $defport
929        }
930
931        if {[Check $selfns {set channel [$fct $comp(host) $comp(port)]}]} then {
932            return 0
933        }
934
935        if {$binddn eq ""} then {
936            set bind 0
937        } else {
938            set bind 1
939            if {[Check $selfns {::ldap::bind $channel $binddn $bindpw}]} then {
940                return 0
941            }
942        }
943        return 1
944    }
945
946    # Disconnect from the LDAP directory
947
948    method disconnect {} {
949
950        Connected $selfns
951
952        if {$bind} {
953            if {[Check $selfns {::ldap::unbind $channel}]} then {
954                return 0
955            }
956        }
957        if {[Check $selfns {::ldap::disconnect $channel}]} then {
958            return 0
959        }
960        set channel ""
961        return 1
962    }
963
964    # New control structure : traverse the DIT and execute the body
965    # for each found entry.
966
967    method traverse {base filter attrs entry body} {
968
969        Connected $selfns
970
971        global errorInfo errorCode
972
973        set lastError ""
974
975        #
976        # Initiate search
977        #
978
979        set opt [list                                             \
980                        -scope        $options(-scope)            \
981                        -derefaliases $options(-derefaliases)     \
982                        -sizelimit    $options(-sizelimit)        \
983                        -timelimit    $options(-timelimit)        \
984                        -attrsonly    $options(-attrsonly)        \
985                        ]
986
987        ::ldap::searchInit $channel $base $filter $attrs $opt
988
989        #
990        # Execute the specific body for each result found
991        #
992
993        while {1} {
994            #
995            # The first call to searchNext may fail when searchInit
996            # is given some invalid parameters.
997            # We must terminate the current search in order to allow
998            # future searches.
999            #
1000
1001            set err [catch {::ldap::searchNext $channel} r]
1002
1003            if {$err} then {
1004                set ei $errorInfo
1005                set ec $errorCode
1006                ::ldap::searchEnd $channel
1007                return -code error -errorinfo $ei -errorcode $ec $r
1008            }
1009
1010            #
1011            # End of result messages
1012            #
1013
1014            if {[llength $r] == 0} then {
1015                break
1016            }
1017
1018            #
1019            # Set DN and attributes-values (converted from utf8 if needed)
1020            # for the entry
1021            #
1022
1023            $entry reset
1024
1025            $entry dn [lindex $r 0]
1026            $entry setall [$translator decodepairs [lindex $r 1]]
1027
1028            #
1029            # Execute body with the entry
1030            #
1031            # http://wiki.tcl.tk/685
1032            #
1033
1034            set code [catch {uplevel 1 $body} msg]
1035            switch -- $code {
1036                0 {
1037                    # ok
1038                }
1039                1 {
1040                    # error
1041                    set ei $errorInfo
1042                    set ec $errorCode
1043                    ::ldap::searchEnd $channel
1044                    return -code error -errorinfo $ei -errorcode $ec $msg
1045                }
1046                2 {
1047                    # return
1048                    ::ldap::searchEnd $channel
1049                    return -code return $msg
1050                }
1051                3 {
1052                    # break
1053                    ::ldap::searchEnd $channel
1054                    return {}
1055                }
1056                4 {
1057                    # continue
1058                }
1059                default {
1060                    # user defined
1061                    ::ldap::searchEnd $channel
1062                    return -code $code $msg
1063                }
1064            }
1065        }
1066
1067        #
1068        # Terminate search
1069        #
1070
1071        ::ldap::searchEnd $channel
1072    }
1073
1074    # Returns a list of newly created objects which match
1075
1076    method search {base filter attrs} {
1077
1078        Connected $selfns
1079
1080        set e [::ldapx::entry create %AUTO%]
1081        set r {}
1082        $self traverse $base $filter $attrs $e {
1083            set new [::ldapx::entry create %AUTO%]
1084            $e backup $new
1085            lappend r $new
1086        }
1087        $e destroy
1088        return $r
1089    }
1090
1091    # Read one or more entries, and returns the number of entries found.
1092    # Useful to easily read one or more entries.
1093
1094    method read {base filter args} {
1095
1096        set n 0
1097        set max [llength $args]
1098        set e [::ldapx::entry create %AUTO%]
1099        $self traverse $base $filter {} $e {
1100            if {$n < $max} then {
1101                $e backup [lindex $args $n]
1102            }
1103            incr n
1104        }
1105        return $n
1106    }
1107
1108    # Commit a list of changes (or standard, backuped entries)
1109
1110    method commit {args} {
1111
1112        Connected $selfns
1113
1114        foreach entry $args {
1115            switch -- [$entry format] {
1116                uninitialized {
1117                    return -code error \
1118                        "Uninitialized entry"
1119                }
1120                standard {
1121                    set echg [::ldapx::entry create %AUTO%]
1122                    set lchg [$echg diff $entry]
1123                    set dn   [$echg dn]
1124                    $echg destroy
1125                }
1126                change {
1127                    set dn   [$entry dn]
1128                    set lchg [$entry change]
1129                }
1130            }
1131
1132            foreach chg $lchg {
1133                set op   [lindex $chg 0]
1134
1135                switch -- $op {
1136                    {} {
1137                        # nothing to do
1138                    }
1139                    add {
1140                        set av [$translator encodepairs [lindex $chg 1]]
1141                        if {[Check $selfns {::ldap::addMulti $channel $dn $av}]} then {
1142                            return 0
1143                        }
1144                    }
1145                    del {
1146                        if {[Check $selfns {::ldap::delete $channel $dn}]} then {
1147                            return 0
1148                        }
1149                    }
1150                    mod {
1151                        set lrep {}
1152                        set ldel {}
1153                        set ladd {}
1154
1155                        foreach submod [lindex $chg 1] {
1156                            set subop [lindex $submod 0]
1157                            set attr [lindex $submod 1]
1158                            set vals [lindex $submod 2]
1159
1160                            set vals [$translator encode $attr $vals]
1161                            switch -- $subop {
1162                                modadd {
1163                                    lappend ladd $attr $vals
1164                                }
1165                                moddel {
1166                                    lappend ldel $attr $vals
1167                                }
1168                                modrepl {
1169                                    lappend lrep $attr $vals
1170                                }
1171                            }
1172                        }
1173
1174                        if {[Check $selfns {::ldap::modifyMulti $channel $dn \
1175                                                    $lrep $ldel $ladd}]} then {
1176                            return 0
1177                        }
1178                    }
1179                    modrdn {
1180                        set newrdn [lindex $chg 1]
1181                        set delOld [lindex $chg 2]
1182                        set newSup [lindex $chg 3]
1183                        if {[string equal $newSup ""]} then {
1184                            if {[Check $selfns {::ldap::modifyDN $channel $dn \
1185                                                    $newrdn $delOld}]} then {
1186                                return 0
1187                            }
1188                        } else {
1189                            if {[Check $selfns {::ldap::modifyDN $channel $dn \
1190                                                    $newrdn $delOld $newSup}]} then {
1191                                return 0
1192                            }
1193                        }
1194                    }
1195                }
1196            }
1197        }
1198
1199        return 1
1200    }
1201
1202    #########################################################################
1203    # Local procedures
1204    #########################################################################
1205
1206    proc Connected {selfns} {
1207        if {$channel eq ""} then {
1208            return -code error \
1209                "Object not connected"
1210        }
1211    }
1212
1213    proc Check {selfns script} {
1214        return [catch {uplevel 1 $script} lastError]
1215    }
1216
1217    #########################################################################
1218    # End of LDAP object type
1219    #########################################################################
1220}
1221
1222##############################################################################
1223# LDIF object type
1224##############################################################################
1225
1226snit::type ::ldapx::ldif {
1227
1228    #########################################################################
1229    # Options
1230    #########################################################################
1231
1232    #
1233    # Fields to ignore when reading change file
1234    #
1235
1236    option -ignore {}
1237
1238    component translator
1239    delegate option -utf8 to translator
1240
1241
1242    #########################################################################
1243    # Variables
1244    #########################################################################
1245
1246    #
1247    # Version of LDIF file (0 means : uninitialized)
1248    #
1249
1250    variable version 0
1251
1252    #
1253    # Channel descriptor
1254    #
1255
1256    variable channel ""
1257
1258    #
1259    # Line number
1260    #
1261
1262    variable lineno 0
1263
1264    #
1265    # Last error message
1266    #
1267
1268    variable lastError ""
1269
1270    #
1271    # Number of entries read or written
1272    #
1273
1274    variable nentries 0
1275
1276    #
1277    # Type of LDIF file
1278    #
1279
1280    variable format "uninitialized"
1281
1282    #########################################################################
1283    # Constructor
1284    #########################################################################
1285
1286    constructor {args} {
1287        install translator using ::ldapx::utf8trans create %AUTO%
1288        $self configurelist $args
1289    }
1290
1291    destructor {
1292        catch {$translator destroy}
1293    }
1294
1295    #########################################################################
1296    # Methods
1297    #########################################################################
1298
1299    # Initialize a channel
1300
1301    method channel {newchan} {
1302
1303        set channel   $newchan
1304        set version   0
1305        set nentries  0
1306        set format    "uninitialized"
1307        set lineno    0
1308        return
1309    }
1310
1311    # Get or set the last error message
1312
1313    method error {{le {-}}} {
1314
1315        if {$le ne "-"} then {
1316            set lastError $le
1317        }
1318        return $lastError
1319    }
1320
1321    # An LDIF file cannot include both changes and standard entries
1322    # (see RFC 2849, page 2). Check this.
1323
1324    method compatible {ref} {
1325
1326        if {$format eq "uninitialized"} then {
1327            set format $ref
1328        } elseif {$format ne $ref} then {
1329            return -code error \
1330                "Invalid entry ($ref) type for LDIF $format file"
1331        }
1332    }
1333
1334    # Reads an LDIF entry (standard or change) from the channel
1335    # returns 1 if ok, 0 if error or EOF
1336
1337    # XXX this method is just coded for tests at this time
1338
1339    method debugread {entry} {
1340
1341        $entry compatible "standard"
1342        $entry dn "uid=joe,ou=org,o=com"
1343        $entry setall {uid {joe} sn {User} givenName {Joe} cn {{Joe User}}
1344            telephoneNumber {+31415926535 +27182818285} objectClass {person}
1345        }
1346        return 1
1347    }
1348
1349    # Read an LDIF entry (standard or change) from the channel
1350    # returns 1 if ok, 0 if error or EOF
1351
1352    method read {entry} {
1353        if {$channel eq ""} then {
1354            return -code error \
1355                        "Channel not initialized"
1356        }
1357
1358        set r [Lexical $selfns]
1359        if {[lindex $r 0] ne "err"} then {
1360            set r [Syntaxic $selfns [lindex $r 1]]
1361        }
1362
1363        if {[lindex $r 0] eq "err"} then {
1364            set lastError [lindex $r 1]
1365            return 0
1366        }
1367
1368        switch -- [lindex $r 0] {
1369            uninitialized {
1370                $entry reset
1371                set lastError ""
1372                set r 0
1373            }
1374            standard {
1375                if {[catch {$self compatible "change"}]} then {
1376                    set lastError "Standard entry not allowed in LDIF change file"
1377                    set r 0
1378                } else {
1379                    $entry reset
1380                    $entry dn     [lindex $r 1]
1381                    $entry setall [lindex $r 2]
1382                    set r 1
1383                }
1384            }
1385            change {
1386                if {[catch {$self compatible "change"}]} then {
1387                    set lastError "Change entry not allowed in LDIF standard file"
1388                    set r 0
1389                } else {
1390                    $entry reset
1391                    $entry dn     [lindex $r 1]
1392                    $entry change [list [lindex $r 2]]
1393                    set r 1
1394                }
1395            }
1396            default {
1397                return -code error \
1398                        "Internal error (invalid returned entry format)"
1399            }
1400        }
1401
1402        return $r
1403    }
1404
1405    # Write an LDIF entry to the channel
1406
1407    method write {entry} {
1408
1409        if {$channel eq ""} then {
1410            return -code error \
1411                        "Channel not initialized"
1412        }
1413
1414        switch -- [$entry format] {
1415            uninitialized {
1416                # nothing
1417            }
1418            standard {
1419                if {[llength [$entry getall]]} then {
1420                    $self compatible "standard"
1421
1422                    if {$nentries == 0} then {
1423                        if {$version == 0} then {
1424                            set version 1
1425                        }
1426                        WriteLine $selfns "version" "$version"
1427                        puts $channel ""
1428                    }
1429
1430                    WriteLine $selfns "dn" [$entry dn]
1431
1432                    foreach a [$entry getattr] {
1433                        foreach v [$entry get $a] {
1434                            WriteLine $selfns $a $v
1435                        }
1436                    }
1437                    puts $channel ""
1438                }
1439            }
1440            change {
1441                $self compatible "change"
1442
1443                set lchg [$entry change]
1444                foreach chg $lchg {
1445                    if {$nentries == 0} then {
1446                        if {$version == 0} then {
1447                            set version 1
1448                        }
1449                        WriteLine $selfns "version" "$version"
1450                        puts $channel ""
1451                    }
1452
1453                    WriteLine $selfns "dn" [$entry dn]
1454
1455                    set op [lindex $chg 0]
1456                    switch -- $op {
1457                        add {
1458                            WriteLine $selfns "changetype" "add"
1459                            foreach {attr vals} [lindex $chg 1] {
1460                                foreach v $vals {
1461                                    WriteLine $selfns $attr $v
1462                                }
1463                            }
1464                        }
1465                        del {
1466                            WriteLine $selfns "changetype" "delete"
1467                        }
1468                        mod {
1469                            WriteLine $selfns "changetype" "modify"
1470                            foreach submod [lindex $chg 1] {
1471                                set subop [lindex $submod 0]
1472                                set attr [lindex $submod 1]
1473                                set vals [lindex $submod 2]
1474
1475                                switch -- $subop {
1476                                    modadd {
1477                                        WriteLine $selfns "add" $attr
1478                                    }
1479                                    moddel {
1480                                        WriteLine $selfns "delete" $attr
1481                                    }
1482                                    modrepl {
1483                                        WriteLine $selfns "replace" $attr
1484                                    }
1485                                }
1486                                foreach v $vals {
1487                                    WriteLine $selfns $attr $v
1488                                }
1489                                puts $channel "-"
1490                            }
1491                        }
1492                        modrdn {
1493                            WriteLine $selfns "changetype" "modrdn"
1494                            set newrdn [lindex $chg 1]
1495                            set delold [lindex $chg 2]
1496                            set newsup [lindex $chg 3]
1497                            WriteLine $selfns "newrdn" $newrdn
1498                            WriteLine $selfns "deleteOldRDN" $delold
1499                            if {$newsup ne ""} then {
1500                                WriteLine $selfns "newSuperior" $newsup
1501                            }
1502                        }
1503                    }
1504                    puts $channel ""
1505                    incr nentries
1506                }
1507            }
1508            default {
1509                return -code error \
1510                        "Invalid entry format"
1511            }
1512        }
1513        return 1
1514    }
1515
1516    #########################################################################
1517    # Local procedures to read an entry
1518    #########################################################################
1519
1520    #
1521    # Lexical analysis of an entry
1522    # Special case for "version:" entry.
1523    # Returns a list of lines {ok {{<attr1> <val1>} {<attr2> <val2>} ...}}
1524    # or a list {err <message>}
1525    #
1526
1527    proc Lexical {selfns} {
1528        set result {}
1529        set prev ""
1530
1531        while {[gets $channel line] > -1} {
1532            incr lineno
1533
1534            if {$line eq ""} then {
1535                #
1536                # Empty line: we are either before the beginning
1537                # of the entry or at the empty line after the
1538                # entry.
1539                # We don't give up before getting something.
1540                #
1541
1542                if {! [FlushLine $selfns "" result prev msg]} then {
1543                    return [list "err" $msg]
1544                }
1545
1546                if {[llength $result]} then {
1547                    break
1548                }
1549
1550            } elseif {[regexp {^[ \t]} $line]} then {
1551                #
1552                # Continuation line
1553                #
1554
1555                append prev [string trim $line]
1556
1557            } elseif {[regexp {^-$} $line]} then {
1558                #
1559                # Separation between individual modifications
1560                #
1561
1562                if {! [FlushLine $selfns "" result prev msg]} then {
1563                    return [list "err" $msg]
1564                }
1565                lappend result [list "-" {}]
1566
1567            } else {
1568                #
1569                # Should be a normal line (key: val)
1570                #
1571
1572                if {! [FlushLine $selfns $line result prev msg]} then {
1573                    return [list "err" $msg]
1574                }
1575
1576            }
1577        }
1578
1579        #
1580        # End of file, or end of entry. Flush buffered data from $prev
1581        # for EOF case.
1582        #
1583
1584        if {! [FlushLine $selfns "" result prev msg]} then {
1585            return [list "err" $msg]
1586        }
1587
1588        return [list "ok" $result]
1589    }
1590
1591    proc FlushLine {selfns line _result _prev _msg} {
1592        upvar $_result result  $_prev prev  $_msg msg
1593
1594        if {$prev ne ""} then {
1595            set r [DecodeLine $selfns $prev]
1596            if {[llength $r] != 2} then {
1597                set msg "$lineno: invalid syntax"
1598                return 0
1599            }
1600
1601            #
1602            # Special case for "version: 1". This code should not
1603            # be in lexical analysis, but this would be too disruptive
1604            # in syntaxic analysis
1605            #
1606
1607            if {[string equal -nocase [lindex $r 0] "version"]} then {
1608                if {$version != 0} then {
1609                    set msg "version attribute allowed only at the beginning of the LDIF file"
1610                    return 0
1611                }
1612                set val [lindex $r 1]
1613                if {[catch {set val [expr {$val+0}]}]} then {
1614                    set msg "invalid version value"
1615                    return 0
1616                }
1617                if {$val != 1} then {
1618                    set msg "unrecognized version '$val'"
1619                    return 0
1620                }
1621                set version 1
1622            } else {
1623                lappend result $r
1624            }
1625        }
1626        set prev $line
1627
1628        return 1
1629    }
1630
1631    proc DecodeLine {selfns str} {
1632        if {[regexp {^([^:]*)::[ \t]*(.*)} $str d key val]} then {
1633            set key [string tolower $key]
1634            set val [::base64::decode $val]
1635            set val [$translator decode $key $val]
1636            set r [list $key $val]
1637        } elseif {[regexp {^([^:]*):[ \t]*(.*)} $str d key val]} then {
1638            set key [string tolower $key]
1639            set val [$translator decode $key $val]
1640            set r [list $key $val]
1641        } else {
1642            # syntax error
1643            set r {}
1644        }
1645        return $r
1646    }
1647
1648    #
1649    # Array indexed by current state of the LDIF automaton
1650    # Each element is a list of actions, each with the format:
1651    #   pattern on on "attribute:value"
1652    #   next state
1653    #   script (to be evaled in Syntaxic local procedure)
1654    #
1655
1656    variable ldifautomaton -array {
1657        begin {
1658            {dn:*               dn              {set dn $val}}
1659            {EOF:*              end             {set r [list "empty"]}}
1660        }
1661        dn {
1662            {changetype:modify  mod             {set t "change" ; set r {mod}}}
1663            {changetype:modrdn  modrdn          {set t "change" ; set newsup {}}}
1664            {changetype:add     add             {set t "change"}}
1665            {changetype:delete  del             {set t "change"}}
1666            {*:*                standard        {set t "standard" ; lappend tab($key) $val}}
1667        }
1668        standard {
1669            {EOF:*              end             {set r [array get tab]}}
1670            {*:*                standard        {lappend tab($key) $val}}
1671        }
1672        mod {
1673            {add:*              mod-add         {set attr [string tolower $val] ; set vals {}}}
1674            {delete:*           mod-del         {set attr [string tolower $val] ; set vals {}}}
1675            {replace:*          mod-repl        {set attr [string tolower $val] ; set vals {}}}
1676            {EOF:*              end             {}}
1677        }
1678        mod-add {
1679            {*:*                mod-add-attr    {lappend vals $val}}
1680        }
1681        mod-add-attr {
1682            {-:*                mod             {lappend r [list "modadd" $attr $vals]}}
1683            {*:*                mod-add-attr    {lappend vals $val}}
1684        }
1685        mod-del {
1686            {-:*                mod             {lappend r [list "moddel" $attr $vals]}}
1687            {*:*                mod-del         {lappend vals $val}}
1688        }
1689        mod-repl {
1690            {-:*                mod             {lappend r [list "modrepl" $attr $vals]}}
1691            {*:*                mod-repl        {lappend vals $val}}
1692        }
1693        modrdn {
1694            {newrdn:*           modrdn-new      {set newrdn $val}}
1695        }
1696        modrdn-new {
1697            {deleteoldrdn:0     modrdn-del      {set delold 0}}
1698            {deleteoldrdn:1     modrdn-del      {set delold 1}}
1699        }
1700        modrdn-del {
1701            {newsuperior:*      modrdn-end      {set newsup $val}}
1702            {EOF:*              end             {set r [list modrdn $newrdn $delold] }}
1703        }
1704        modrdn-end {
1705            {EOF:*              end             {set r [list modrdn $newrdn
1706                                                    $delold $newsup]}}
1707        }
1708        add {
1709            {EOF:*              end             {set r [list add [array get tab]]}}
1710            {*:*                add             {lappend tab($key) $val}}
1711        }
1712        del {
1713            {EOF:*              end             {set r [list del]}}
1714        }
1715    }
1716
1717    proc Syntaxic {selfns lcouples} {
1718        set state "begin"
1719        set newsup {}
1720        set t "uninitialized"
1721        foreach c $lcouples {
1722            set key [lindex $c 0]
1723            if {[lsearch [string tolower $options(-ignore)] $key] == -1} then {
1724                set val [lindex $c 1]
1725                set a [Automaton $selfns $state $key $val]
1726                if {$a eq ""} then {
1727                    return [list "err" "Syntax error before line $lineno"]
1728                }
1729                set state [lindex $a 0]
1730                set script [lindex $a 1]
1731                eval $script
1732            }
1733        }
1734
1735        set a [Automaton $selfns $state "EOF" "EOF"]
1736        if {$a eq ""} then {
1737            return [list "err" "Premature EOF"]
1738        }
1739        set script [lindex $a 1]
1740        eval $script
1741
1742        set result [list $t]
1743        switch $t {
1744            uninitialized {
1745                # nothing
1746            }
1747            standard {
1748                lappend result $dn $r
1749            }
1750            change {
1751                lappend result $dn $r
1752            }
1753        }
1754
1755        return $result
1756    }
1757
1758    proc Automaton {selfns state key val} {
1759        set r {}
1760        if {[info exists ldifautomaton($state)]} then {
1761            foreach a $ldifautomaton($state) {
1762                if {[string match [lindex $a 0] "$key:$val"]} then {
1763                    set r [lreplace $a 0 0]
1764                    break
1765                }
1766            }
1767        }
1768        return $r
1769    }
1770
1771    #########################################################################
1772    # Local procedures to write an entry
1773    #########################################################################
1774
1775    proc WriteLine {selfns attr val} {
1776
1777        if {[string is ascii $val] && [string is print $val]} then {
1778            set sep ":"
1779        } else {
1780            set sep "::"
1781            set val [$translator encode $attr $val]
1782            set val [::base64::encode $val]
1783        }
1784
1785        set first 1
1786        foreach line [split $val "\n"] {
1787            if {$first} then {
1788                puts $channel "$attr$sep $line"
1789                set first 0
1790            } else {
1791                puts $channel " $line"
1792            }
1793        }
1794    }
1795}
Note: See TracBrowser for help on using the repository browser.