1# -*- coding: ascii -*-
2"""
3web2ldap.app.dit: do a tree search and display to the user
4
5web2ldap - a web-based LDAP Client,
6see https://www.web2ldap.de for details
7
8(c) 1998-2021 by Michael Stroeder <michael@stroeder.com>
9
10This software is distributed under the terms of the
11Apache License Version 2.0 (Apache-2.0)
12https://www.apache.org/licenses/LICENSE-2.0
13"""
14
15import ldap0
16from ldap0.dn import DNObj
17
18from ..ldaputil import has_subordinates
19from .gui import dn_anchor_hash, main_menu
20
21
22# All attributes to be read for nodes
23DIT_ATTR_LIST = [
24    'objectClass',
25    'structuralObjectClass',
26    'displayName',
27    'description',
28    'hasSubordinates',
29    'subordinateCount',
30    'numSubordinates',
31    #  Siemens DirX
32    'numAllSubordinates',
33    # Critical Path Directory Server
34    'countImmSubordinates',
35    'countTotSubordinates',
36    # MS Active Directory
37    'msDS-Approx-Immed-Subordinates',
38]
39
40
41def dit_html(app, anchor_dn, dit_dict, entry_dict, max_levels):
42    """
43    Outputs HTML representation of a directory information tree (DIT)
44    """
45
46    assert isinstance(anchor_dn, DNObj), ValueError(
47        'Expected anchor_dn to be DNObj, got %r' % (anchor_dn,),
48    )
49
50    # Start node's HTML
51    res = ['<dl>']
52
53    for dn, ddat in dit_dict.items():
54
55        assert isinstance(dn, DNObj), ValueError(
56            'Expected dn to be DNObj, got %r' % (dn,),
57        )
58
59        try:
60            size_limit = ddat['_sizelimit_']
61        except KeyError:
62            size_limit = False
63        else:
64            del ddat['_sizelimit_']
65
66        # Generate anchor for this node
67        if dn:
68            rdn = dn.rdn()
69        else:
70            rdn = 'Root DSE'
71
72        try:
73            node_entry = entry_dict[dn]
74        except KeyError:
75            # Try to read the missing entry
76            try:
77                ldap_res = app.ls.l.read_s(str(dn), attrlist=DIT_ATTR_LIST)
78            except ldap0.LDAPError:
79                node_entry = {}
80            else:
81                node_entry = {} if ldap_res is None else ldap_res.entry_s
82
83        if size_limit:
84            partial_str = '<strong>...</strong>'
85        else:
86            partial_str = ''
87
88        try:
89            display_name_list = [app.form.s2d(node_entry['displayName'][0]), partial_str]
90        except KeyError:
91            display_name_list = [app.form.s2d(str(rdn)), partial_str]
92        display_name = ''.join(display_name_list)
93
94        title_msg = '\r\n'.join(
95            (str(dn) or 'Root DSE', node_entry.get('structuralObjectClass', [''])[0]) + \
96            tuple(node_entry.get('description', []))
97        )
98
99        dn_anchor_id = dn_anchor_hash(dn)
100
101        res.append('<dt id="%s">' % (app.form.s2d(dn_anchor_id)))
102        if has_subordinates(node_entry, default=True):
103            if dn == anchor_dn:
104                link_text = '&lsaquo;&lsaquo;'
105                next_dn = dn.parent()
106            else:
107                link_text = '&rsaquo;&rsaquo;'
108                next_dn = dn
109            # Only display link if there are subordinate entries expected or unknown
110            res.append(
111                app.anchor(
112                    'dit', link_text,
113                    [('dn', str(next_dn))],
114                    title='Browse from %s' % (str(next_dn),),
115                    anchor_id=dn_anchor_id,
116                )
117            )
118        else:
119            # FIX ME! Better solution in pure CSS?
120            res.append('&nbsp;&nbsp;&nbsp;&nbsp;')
121        res.append('<span title="%s">%s</span>' % (
122            app.form.s2d(title_msg),
123            display_name
124        ))
125        res.append(
126            app.anchor(
127                'read', '&rsaquo;',
128                [('dn', str(dn))],
129                title='Read entry',
130            )
131        )
132        res.append('</dt>')
133
134        # Subordinate nodes' HTML
135        res.append('<dd>')
136        if max_levels and ddat:
137            res.extend(dit_html(app, anchor_dn, ddat, entry_dict, max_levels-1))
138        res.append('</dd>')
139
140    # Finish node's HTML
141    res.append('</dl>')
142
143    return res # dit_html()
144
145
146def w2l_dit(app):
147
148    dit_dict = {}
149    entry_dict = {}
150
151    root_dit_dict = dit_dict
152
153    dn_levels = len(app.dn_obj)
154    dit_max_levels = app.cfg_param('dit_max_levels', 10)
155    cut_off_levels = max(0, dn_levels-dit_max_levels)
156
157    for i in range(1, dn_levels-cut_off_levels+1):
158        search_base = app.dn_obj.slice(dn_levels-cut_off_levels-i, None)
159        dit_dict[search_base] = {}
160        try:
161            msg_id = app.ls.l.search(
162                str(search_base),
163                ldap0.SCOPE_ONELEVEL,
164                '(objectClass=*)',
165                attrlist=DIT_ATTR_LIST,
166                timeout=app.cfg_param('dit_search_timelimit', 10),
167                sizelimit=app.cfg_param('dit_search_sizelimit', 50),
168            )
169            for ldap_result in app.ls.l.results(msg_id):
170                # FIX ME! Search continuations are ignored for now
171                if ldap_result.rtype == ldap0.RES_SEARCH_REFERENCE:
172                    continue
173                for res in ldap_result.rdata:
174                    entry_dict[res.dn_o] = res.entry_s
175                    dit_dict[search_base][res.dn_o] = {}
176        except (
177                ldap0.TIMEOUT,
178                ldap0.SIZELIMIT_EXCEEDED,
179                ldap0.TIMELIMIT_EXCEEDED,
180                ldap0.ADMINLIMIT_EXCEEDED,
181                ldap0.NO_SUCH_OBJECT,
182                ldap0.INSUFFICIENT_ACCESS,
183                ldap0.PARTIAL_RESULTS,
184                ldap0.REFERRAL,
185            ):
186            dit_dict[search_base]['_sizelimit_'] = True
187        else:
188            dit_dict[search_base]['_sizelimit_'] = False
189        dit_dict = dit_dict[search_base]
190
191    if root_dit_dict:
192        outf_lines = dit_html(
193            app,
194            app.dn_obj,
195            root_dit_dict,
196            entry_dict,
197            dit_max_levels,
198        )
199    else:
200        if app.dn:
201            outf_lines = ['No results.']
202        else:
203            outf_lines = ['<p>No results for root search.</p>']
204            for naming_context in app.ls.namingContexts:
205                outf_lines.append(
206                    '<p>%s %s</p>' % (
207                        app.anchor(
208                            'dit', '&rsaquo;&rsaquo;',
209                            (('dn', str(naming_context)),),
210                            title='Display tree beneath %s' % (naming_context,),
211                        ),
212                        app.form.s2d(str(naming_context)),
213                    )
214                )
215
216    app.simple_message(
217        'Tree view',
218        """
219        <h1>Directory Information Tree</h1>
220        <div id="DIT">%s</div>
221        """ % ('\n'.join(outf_lines)),
222        main_menu_list=main_menu(app),
223        context_menu_list=[]
224    )
225
226    # end of w2l_dit()
227