1# -*- coding: ascii -*-
2"""
3web2ldap.app.searchform: different search forms
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
16
17import web2ldapcnf
18
19from ..web.forms import Select as SelectField
20from ..log import logger
21
22from . import ErrorExit
23from .gui import attrtype_select_field, search_root_field
24from .gui import footer, main_menu, top_section
25from .tmpl import get_variant_filename
26
27
28SEARCHFORM_MODE_TEXT = {
29    'adv': 'Advanced',
30    'base': 'Basic',
31    'exp': 'Expert',
32}
33
34SEARCH_OPT_CONTAINS = '({at}=*{av}*)'
35SEARCH_OPT_DOESNT_CONTAIN = '(!({at}=*{av}*))'
36SEARCH_OPT_ATTR_EXISTS = '({at}=*)'
37SEARCH_OPT_ATTR_NOT_EXISTS = '(!({at}=*))'
38SEARCH_OPT_IS_EQUAL = '({at}={av})'
39SEARCH_OPT_IS_NOT = '(!({at}={av}))'
40SEARCH_OPT_BEGINS_WITH = '({at}={av}*)'
41SEARCH_OPT_ENDS_WITH = '({at}=*{av})'
42SEARCH_OPT_SOUNDS_LIKE = '({at}~={av})'
43SEARCH_OPT_GE_THAN = '({at}>={av})'
44SEARCH_OPT_LE_THAN = '({at}<={av})'
45SEARCH_OPT_DN_ATTR_IS = '({at}:dn:={av})'
46SEARCH_OPT_DN_SUBORDINATE = '({at}:dnSubordinateMatch:={av})'
47SEARCH_OPT_DN_SUBTREE = '({at}:dnSubtreeMatch:={av})'
48SEARCH_OPT_DN_ONE_LEVEL = '({at}:dnOneLevelMatch:={av})'
49
50SEARCH_SCOPE_STR_BASE = '0'
51SEARCH_SCOPE_STR_ONELEVEL = '1'
52SEARCH_SCOPE_STR_SUBTREE = '2'
53SEARCH_SCOPE_STR_SUBORDINATES = '3'
54
55SEARCH_SCOPE_OPTIONS = [
56    (str(ldap0.SCOPE_BASE), 'Base'),
57    (str(ldap0.SCOPE_ONELEVEL), 'One level'),
58    (str(ldap0.SCOPE_SUBTREE), 'Sub tree'),
59    (str(ldap0.SCOPE_SUBORDINATE), 'Subordinate'),
60]
61
62SEARCH_OPTIONS = (
63    (SEARCH_OPT_IS_EQUAL, 'attribute value is'),
64    (SEARCH_OPT_CONTAINS, 'attribute value contains'),
65    (SEARCH_OPT_DOESNT_CONTAIN, 'attribute value does not contain'),
66    (SEARCH_OPT_IS_NOT, 'attribute value is not'),
67    (SEARCH_OPT_BEGINS_WITH, 'attribute value begins with'),
68    (SEARCH_OPT_ENDS_WITH, 'attribute value ends with'),
69    (SEARCH_OPT_SOUNDS_LIKE, 'attribute value sounds like'),
70    (SEARCH_OPT_GE_THAN, 'attribute value greater equal than'),
71    (SEARCH_OPT_LE_THAN, 'attribute value lesser equal than'),
72    (SEARCH_OPT_DN_ATTR_IS, 'DN attribute value is'),
73    (SEARCH_OPT_ATTR_EXISTS, 'entry has attribute'),
74    (SEARCH_OPT_ATTR_NOT_EXISTS, 'entry does not have attribute'),
75    (SEARCH_OPT_DN_SUBORDINATE, 'DN is subordinate of'),
76    (SEARCH_OPT_DN_SUBTREE, 'DN within subtree'),
77    (SEARCH_OPT_DN_ONE_LEVEL, 'DN is direct child of'),
78)
79
80FILTERSTR_FIELDSET_TMPL = """
81<fieldset>
82  <legend>LDAP filter string</legend>
83  <input name="filterstr" maxlength="%d" size="%d" value="%s">
84</fieldset>
85"""
86
87
88def search_form_exp(app, filterstr=''):
89    """
90    Output expert search form
91    """
92    filterstr = app.form.getInputValue('filterstr', [filterstr])[0]
93    result = FILTERSTR_FIELDSET_TMPL % (
94        app.form.field['filterstr'].maxLen,
95        app.form.field['filterstr'].size,
96        app.form.s2d(filterstr),
97    )
98    return result
99
100
101def search_form_base(app, searchform_template_name):
102    """
103    Output basic search form based on a HTML template configured
104    with host-specific configuration parameter searchform_template
105    """
106    searchform_template_cfg = app.cfg_param('searchform_template', '')
107    searchform_template = searchform_template_cfg.get(searchform_template_name, None)
108    searchform_template_filename = get_variant_filename(
109        searchform_template,
110        app.form.accept_language,
111    )
112    with open(searchform_template_filename, 'rb') as fileobj:
113        template_str = fileobj.read().decode('utf-8')
114    return template_str
115
116
117def search_form_adv(app):
118    """advanced search form with select lists"""
119
120    search_submit = app.form.getInputValue('search_submit', [''])[0]
121
122    # Get input values
123    search_attr_list = app.form.getInputValue('search_attr', [''])
124    search_option_list = app.form.getInputValue('search_option', [None]*len(search_attr_list))
125    search_mr_list = app.form.getInputValue('search_mr', [None]*len(search_attr_list))
126    search_string_list = app.form.getInputValue('search_string', ['']*len(search_attr_list))
127
128    if search_submit.startswith('-'):
129        del_row_num = int(search_submit[1:])
130        if len(search_attr_list) > 1:
131            del search_option_list[del_row_num]
132            del search_attr_list[del_row_num]
133            del search_mr_list[del_row_num]
134            del search_string_list[del_row_num]
135    elif search_submit.startswith('+'):
136        insert_row_num = int(search_submit[1:])
137        if len(search_attr_list) < web2ldapcnf.max_searchparams:
138            search_option_list.insert(insert_row_num+1, search_option_list[insert_row_num])
139            search_attr_list.insert(insert_row_num+1, search_attr_list[insert_row_num])
140            search_mr_list.insert(insert_row_num+1, search_mr_list[insert_row_num])
141            search_string_list.insert(insert_row_num+1, '')
142
143    if not len(search_option_list) == len(search_attr_list) == len(search_string_list):
144        raise ErrorExit('Invalid search form data.')
145
146    search_mode = app.form.getInputValue('search_mode', ['(&%s)'])[0]
147
148    search_mode_select = SelectField(
149        'search_mode', 'Search mode', 1,
150        options=[
151            ('(&%s)', 'all'),
152            ('(|%s)', 'any'),
153        ],
154        default=search_mode
155    )
156    search_mode_select.charset = app.form.accept_charset
157
158    search_attr_select = attrtype_select_field(
159        app,
160        'search_attr',
161        'Search attribute type',
162        search_attr_list,
163        default_attr_options=app.cfg_param('search_attrs', [])
164    )
165
166    mr_list = [''] + sorted(app.schema.name2oid[ldap0.schema.models.MatchingRule].keys())
167    # Create a select field instance for matching rule name
168    search_mr_select = SelectField(
169        'search_mr', 'Matching rule used',
170        web2ldapcnf.max_searchparams,
171        options=mr_list,
172    )
173    search_mr_select.charset = app.form.accept_charset
174
175    search_fields_html_list = []
176
177    # Output a row of the search form
178    for i in range(len(search_attr_list)):
179        search_fields_html_list.append('\n'.join((
180            '<tr>\n<td rowspan="2">',
181            '<button type="submit" name="search_submit" value="+%d">+</button>' % (i),
182            '<button type="submit" name="search_submit" value="-%d">-</button>' % (i),
183            '</td>\n<td>',
184            search_attr_select.input_html(default=search_attr_list[i]),
185            search_mr_select.input_html(default=search_mr_list[i]),
186            app.form.field['search_option'].input_html(default=search_option_list[i]),
187            '</td></tr>\n<tr><td>',
188            app.form.field['search_string'].input_html(default=search_string_list[i]),
189            '</td></tr>',
190        )))
191
192    # Eigentliches Suchformular ausgeben
193    result = """
194    <fieldset>
195      <legend>Search filter parameters</legend>
196      Match %s of the following.<br>
197      <table>%s</table>
198    </fieldset>
199    """ % (
200        search_mode_select.input_html(),
201        '\n'.join(search_fields_html_list),
202    )
203    return result
204
205
206def w2l_searchform(
207        app,
208        msg='',
209        filterstr='',
210        scope=ldap0.SCOPE_SUBTREE,
211        search_root=None,
212        searchform_mode=None,
213    ):
214    """Output a search form"""
215
216    if msg:
217        msg_html = '<p class="ErrorMessage">%s</p>' % (msg)
218    else:
219        msg_html = ''
220
221    searchform_mode = searchform_mode or app.form.getInputValue('searchform_mode', ['base'])[0]
222    searchform_template_name = app.form.getInputValue('searchform_template', ['_'])[0]
223
224    search_root = app.form.getInputValue(
225        'search_root',
226        [search_root or str(app.naming_context)],
227    )[0]
228    srf = search_root_field(
229        app,
230        name='search_root',
231        default=search_root,
232        search_root_searchurl=app.cfg_param('searchform_search_root_url', None),
233    )
234
235    ctx_menu_items = [
236        app.anchor(
237            'searchform', SEARCHFORM_MODE_TEXT[mode],
238            [
239                ('dn', app.dn),
240                ('searchform_mode', mode),
241                ('search_root', search_root),
242                ('filterstr', filterstr),
243                ('scope', str(scope)),
244            ],
245        )
246        for mode in SEARCHFORM_MODE_TEXT
247        if mode != searchform_mode
248    ]
249
250    searchform_template_cfg = app.cfg_param('searchform_template', '')
251    if isinstance(searchform_template_cfg, dict):
252        for sftn in searchform_template_cfg.keys():
253            if sftn != '_':
254                ctx_menu_items.append(app.anchor(
255                    'searchform', app.form.s2d(sftn),
256                    [
257                        ('dn', app.dn),
258                        ('searchform_mode', 'base'),
259                        ('searchform_template', sftn),
260                        ('search_root', search_root),
261                        ('filterstr', filterstr),
262                        ('scope', str(scope)),
263                    ],
264                ))
265
266    if searchform_mode == 'base':
267        # base search form with fixed input fields
268        try:
269            inner_searchform_html = search_form_base(app, searchform_template_name)
270        except IOError as err:
271            logger.warning('Error loading search form template: %s', err)
272            msg_html = '\n'.join((
273                msg_html,
274                '<p class="ErrorMessage">I/O error while loading search form template!</p>'
275            ))
276            inner_searchform_html = search_form_adv(app)
277            searchform_mode = 'adv'
278    elif searchform_mode == 'exp':
279        # expert search form with single filter input field
280        inner_searchform_html = search_form_exp(app, filterstr)
281    elif searchform_mode == 'adv':
282        # base search form with fixed input fields
283        inner_searchform_html = search_form_adv(app)
284
285    searchoptions_template_filename = get_variant_filename(
286        app.cfg_param('searchoptions_template', None),
287        app.form.accept_language
288    )
289    with open(searchoptions_template_filename, 'r') as template_file:
290        searchoptions_template_str = template_file.read()
291
292    top_section(
293        app,
294        '%s Search Form' % SEARCHFORM_MODE_TEXT[searchform_mode],
295        main_menu(app),
296        context_menu_list=ctx_menu_items,
297        main_div_id='Input'
298    )
299
300    app.outf.write(
301        """
302        {msg_html}
303        {form_search_html}
304          <input type="hidden" name="searchform_mode" value="{searchform_mode}">
305          <input type="hidden" name="searchform_template" value="{searchform_template}">
306          <input type="hidden" name="search_output" value="{search_output}">
307          <p>
308            <input type="submit" name="search_submit" value="Search">
309            <input type="reset" value="Reset">
310          </p>
311          {inner_searchform_html}
312          {form_dn_html}
313          {searchoptions_template_str}
314        </form>
315        """.format(
316            form_search_html=app.begin_form('search', 'GET'),
317            searchform_mode=app.form.s2d(searchform_mode),
318            searchform_template=app.form.s2d(searchform_template_name),
319            search_output=app.form.getInputValue('search_output', ['table'])[0],
320            msg_html=msg_html,
321            inner_searchform_html=inner_searchform_html,
322            form_dn_html=app.form.hidden_field_html('dn', app.dn, ''),
323            searchoptions_template_str=searchoptions_template_str.format(
324                field_search_root=srf.input_html(),
325                field_search_scope=app.form.field['scope'].input_html(
326                    default=app.form.getInputValue('scope', [str(scope)])[0]
327                ),
328                field_search_resnumber=app.form.field['search_resnumber'].input_html(
329                    default=app.form.getInputValue(
330                        'search_resnumber',
331                        [str(app.cfg_param('search_resultsperpage', 10))],
332                    )[0]
333                ),
334                field_search_lastmod=app.form.field['search_lastmod'].input_html(
335                    default=app.form.getInputValue('search_lastmod', [str(-1)])[0]
336                ),
337                value_search_attrs=app.form.s2d(app.form.getInputValue('search_attrs', [''])[0]),
338            ),
339        )
340    )
341
342    footer(app)
343