1# -*- coding: ascii -*-
2"""
3web2ldap.app.form: class for web2ldap input form handling
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 http.cookies
16
17import ldap0.ldif
18import ldap0.schema
19from ldap0.pw import random_string
20
21import web2ldapcnf
22
23from ..ldapsession import AVAILABLE_BOOLEAN_CONTROLS, CONTROL_TREEDELETE
24from ..web import HTML_ESCAPE_MAP
25from ..ldaputil import RDN_PATTERN, ATTR_TYPE_PATTERN
26from ..ldaputil.oidreg import OID_REG
27from ..ldaputil.passwd import AVAIL_USERPASSWORD_SCHEMES
28from .gui import HIDDEN_FIELD
29from .searchform import (
30    SEARCH_OPTIONS,
31    SEARCH_SCOPE_OPTIONS,
32    SEARCH_SCOPE_STR_SUBTREE,
33)
34from ..web.forms import (
35    Input,
36    Field,
37    Textarea,
38    BytesInput,
39    Select,
40    Checkbox,
41    Form,
42    InvalidValueFormat,
43)
44from ..web.session import SESSION_ID_CHARS, SESSION_ID_LENGTH, SESSION_ID_REGEX
45
46
47# Work around https://bugs.python.org/issue29613
48http.cookies.Morsel._reserved['samesite'] = 'SameSite'
49
50class Web2LDAPForm(Form):
51    """
52    Form sub-class for a web2ldap use-case
53
54    more sub-classes define forms for different URL commands
55    """
56    command = None
57    cookie_length = web2ldapcnf.cookie_length or 2 * 42
58    cookie_max_age = web2ldapcnf.cookie_max_age
59    cookie_domain = web2ldapcnf.cookie_domain
60    cookie_name_prefix = 'web2ldap_'
61
62    def __init__(self, inf, env):
63        Form.__init__(self, inf, env)
64        # Cookie handling
65        try:
66            self.cookies = http.cookies.SimpleCookie(self.env['HTTP_COOKIE'])
67        except KeyError:
68            self.cookies = http.cookies.SimpleCookie()
69        self.next_cookie = http.cookies.SimpleCookie()
70
71    @staticmethod
72    def s2d(
73            value,
74            tab_identiation='',
75            sp_entity='&nbsp;&nbsp;',
76            lf_entity='\n'
77        ):
78        assert isinstance(value, str), \
79            TypeError('Argument value must be str, was %r' % (value,))
80        value = value or ''
81        translate_map = dict(HTML_ESCAPE_MAP.items())
82        translate_map.update({
83            9: tab_identiation,
84            10: lf_entity,
85        })
86        return value.translate(translate_map).replace('  ', sp_entity)
87
88    def unset_cookie(self, cki):
89        if cki is not None:
90            assert len(cki) == 1, \
91                ValueError(
92                    'More than one Morsel cookie instance in cki: %d objects found' % (len(cki))
93                )
94            cookie_name = list(cki.keys())[0]
95            cki[cookie_name] = ''
96            cki[cookie_name]['max-age'] = 0
97            self.next_cookie.update(cki)
98        # end of unset_cookie()
99
100    def get_cookie_domain(self):
101        if self.cookie_domain:
102            cookie_domain = self.cookie_domain
103        elif 'SERVER_NAME' in self.env or 'HTTP_HOST' in self.env:
104            cookie_domain = self.env.get('HTTP_HOST', self.env['SERVER_NAME']).split(':')[0]
105        return cookie_domain
106
107    def set_cookie(self, name_suffix):
108        # Generate a randomized key and value
109        cookie_key = random_string(
110            alphabet=SESSION_ID_CHARS,
111            length=self.cookie_length,
112        )
113        cookie_name = ''.join((self.cookie_name_prefix, name_suffix))
114        cki = http.cookies.SimpleCookie({
115            cookie_name: cookie_key,
116        })
117        cki[cookie_name]['path'] = self.script_name
118        cki[cookie_name]['domain'] = self.get_cookie_domain()
119        cki[cookie_name]['max-age'] = str(self.cookie_max_age)
120        cki[cookie_name]['httponly'] = True
121        cki[cookie_name]['samesite'] = 'Strict'
122        if self.env.get('HTTPS', None) == 'on':
123            cki[cookie_name]['secure'] = True
124        self.next_cookie.update(cki)
125        return cki # set_cookie()
126
127    def fields(self):
128        return [
129            Input(
130                'delsid',
131                'Old SID to be deleted',
132                SESSION_ID_LENGTH,
133                1,
134                SESSION_ID_REGEX,
135            ),
136            Input('who', 'Bind DN/AuthcID', 1000, 1, '.*', size=40),
137            Input('cred', 'with Password', 200, 1, '.*', size=15),
138            Select(
139                'login_authzid_prefix',
140                'SASL AuthzID',
141                1,
142                options=[('', '- no prefix -'), ('u:', 'user-ID'), ('dn:', 'DN')],
143                default=None
144            ),
145            Input('login_authzid', 'SASL AuthzID', 1000, 1, '.*', size=20),
146            Input('login_realm', 'SASL Realm', 1000, 1, '.*', size=20),
147            AuthMechSelect('login_mech', 'Authentication mechanism'),
148            Input('ldapurl', 'LDAP Url', 1024, 1, '[ ]*ldap(|i|s)://.*', size=30),
149            Input(
150                'host', 'Host:Port',
151                255, 1,
152                '[a-zA-Z0-9_.:\\[\\]-]+',
153                size=70,
154            ),
155            DistinguishedNameInput('dn', 'Distinguished Name'),
156            Select(
157                'scope', 'Scope', 1,
158                options=SEARCH_SCOPE_OPTIONS,
159                default=SEARCH_SCOPE_STR_SUBTREE,
160            ),
161            DistinguishedNameInput('login_search_root', 'Login search root'),
162            Select(
163                'conntype', 'Connection type', 1,
164                options=[
165                    ('0', 'LDAP clear-text connection'),
166                    ('1', 'LDAP with StartTLS ext.op.'),
167                    ('2', 'LDAP over separate SSL port (LDAPS)'),
168                    ('3', 'LDAP over Unix domain socket (LDAPI)')
169                ],
170                default='0',
171            )
172        ]
173
174    def action_url(self, command, sid):
175        return '%s/%s%s' % (
176            self.script_name,
177            command,
178            {False:'/%s' % sid, True:''}[sid is None],
179        )
180
181    def begin_form(
182            self,
183            command,
184            sid,
185            method,
186            target=None,
187            enctype='application/x-www-form-urlencoded',
188        ):
189        target = {
190            False:'target="%s"' % (target),
191            True:'',
192        }[target is None]
193        return """
194          <form
195            action="%s"
196            method="%s"
197            %s
198            enctype="%s"
199            accept-charset="%s"
200          >
201          """  % (
202              self.action_url(command, sid),
203              method,
204              target,
205              enctype,
206              self.accept_charset
207          )
208
209    def hidden_field_html(self, name, value, desc):
210        return HIDDEN_FIELD % (
211            name,
212            self.s2d(value, sp_entity='  '),
213            self.s2d(desc, sp_entity='&nbsp;&nbsp;'),
214        )
215
216    def hidden_input_html(self, ignored_fields=None):
217        """
218        Return all input parameters as hidden fields in one HTML string.
219
220        ignored_fields
221            Names of parameters to be excluded.
222        """
223        ignored_fields = set(ignored_fields or [])
224        res = []
225        for fname in self.input_field_names:
226            if fname in ignored_fields:
227                continue
228            for val in self.field[fname].value:
229                if isinstance(val, str):
230                    res.append(self.hidden_field_html(fname, val, ''))
231                else:
232                    res.append(self.hidden_field_html(fname, '', ''))
233        return '\n'.join(res)
234
235
236class SearchAttrs(Input):
237
238    def __init__(self, name='search_attrs', text='Attributes to be read'):
239        Input.__init__(self, name, text, 1000, 1, '[@*+0-9.\\w,_;-]+')
240
241    def set_value(self, value):
242        value = ','.join(
243            filter(
244                None,
245                map(str.strip, value.replace(' ', ',').split(','))
246            )
247        )
248        Input.set_value(self, value)
249
250
251class Web2LDAPFormSearchform(Web2LDAPForm):
252    command = 'searchform'
253
254    def fields(self):
255        res = Web2LDAPForm.fields(self)
256        res.extend([
257            Input(
258                'search_submit', 'Search form submit button',
259                6, 1,
260                '(Search|[+-][0-9]+)',
261            ),
262            Select(
263                'searchform_mode',
264                'Search form mode',
265                1,
266                options=[('base', 'Base'), ('adv', 'Advanced'), ('exp', 'Expert')],
267                default='base',
268            ),
269            DistinguishedNameInput('search_root', 'Search root'),
270            Input(
271                'filterstr',
272                'Search filter string',
273                1200,
274                1,
275                '.*',
276                size=90,
277            ),
278            Input(
279                'searchform_template',
280                'Search form template name',
281                60,
282                web2ldapcnf.max_searchparams,
283                '[a-zA-Z0-9. ()_-]+',
284            ),
285            Select(
286                'search_resnumber', 'Number of results to display', 1,
287                options=[
288                    ('0', 'unlimited'), ('10', '10'), ('20', '20'),
289                    ('50', '50'), ('100', '100'), ('200', '200'),
290                ],
291                default='10'
292            ),
293            Select(
294                'search_lastmod', 'Interval of last creation/modification', 1,
295                options=[
296                    ('-1', '-'),
297                    ('10', '10 sec.'),
298                    ('60', '1 min.'),
299                    ('600', '10 min.'),
300                    ('3600', '1 hour'),
301                    ('14400', '4 hours'),
302                    ('43200', '12 hours'),
303                    ('86400', '24 hours'),
304                    ('172800', '2 days'),
305                    ('604800', '1 week'),
306                    ('2419200', '4 weeks'),
307                    ('6048000', '10 weeks'),
308                    ('31536000', '1 year'),
309                ],
310                default='-1'
311            ),
312            InclOpAttrsCheckbox(default='yes', checked=False),
313            Select('search_mode', 'Search Mode', 1, options=['(&%s)', '(|%s)']),
314            Input(
315                'search_attr',
316                'Attribute(s) to be searched',
317                100,
318                web2ldapcnf.max_searchparams,
319                '[\\w,_;-]+',
320            ),
321            Input(
322                'search_mr', 'Matching Rule',
323                100,
324                web2ldapcnf.max_searchparams,
325                '[\\w,_;-]+',
326            ),
327            Select(
328                'search_option', 'Search option',
329                web2ldapcnf.max_searchparams,
330                options=SEARCH_OPTIONS,
331            ),
332            Input(
333                'search_string', 'Search string',
334                600,
335                web2ldapcnf.max_searchparams,
336                '.*',
337                size=60,
338            ),
339            SearchAttrs(),
340            ExportFormatSelect(),
341        ])
342        return res
343
344
345class Web2LDAPFormSearch(Web2LDAPFormSearchform):
346    command = 'search'
347
348    def fields(self):
349        res = Web2LDAPFormSearchform.fields(self)
350        res.extend([
351            Input(
352                'search_resminindex',
353                'Minimum index of search results',
354                10, 1,
355                '[0-9]+',
356            ),
357            Input(
358                'search_resnumber',
359                'Number of results to display',
360                3, 1,
361                '[0-9]+',
362            ),
363        ])
364        return res
365
366
367class Web2LDAPFormConninfo(Web2LDAPForm):
368    command = 'conninfo'
369
370    def fields(self):
371        res = Web2LDAPForm.fields(self)
372        res.append(
373            Select(
374                'conninfo_flushcaches',
375                'Flush caches',
376                1,
377                options=('0', '1'),
378                default='0',
379            )
380        )
381        return res
382
383
384class Web2LDAPFormParams(Web2LDAPForm):
385    command = 'params'
386
387    def fields(self):
388        res = Web2LDAPForm.fields(self)
389        res.extend([
390            Select(
391                'params_all_controls',
392                'List all controls',
393                1,
394                options=('0', '1'),
395                default='0',
396            ),
397            Input(
398                'params_enable_control',
399                'Enable LDAPv3 Boolean Control',
400                50, 1,
401                '([0-9]+.)*[0-9]+',
402            ),
403            Input(
404                'params_disable_control',
405                'Disable LDAPv3 Boolean Control',
406                50, 1,
407                '([0-9]+.)*[0-9]+',
408            ),
409            Select(
410                'ldap_deref',
411                'Dereference aliases',
412                maxValues=1,
413                default=str(ldap0.DEREF_NEVER),
414                options=[
415                    (str(ldap0.DEREF_NEVER), 'never'),
416                    (str(ldap0.DEREF_SEARCHING), 'searching'),
417                    (str(ldap0.DEREF_FINDING), 'finding'),
418                    (str(ldap0.DEREF_ALWAYS), 'always'),
419                ]
420            ),
421        ])
422        return res
423
424
425class Web2LDAPFormInput(Web2LDAPForm):
426
427    """Base class for entry data input not directly used"""
428    def fields(self):
429        res = Web2LDAPForm.fields(self)
430        res.extend([
431            Input('in_oc', 'Object classes', 60, 40, '[a-zA-Z0-9.-]+'),
432            Select(
433                'in_ft', 'Type of input form',
434                1,
435                options=('Template', 'Table', 'LDIF', 'OC'),
436                default='Template',
437            ),
438            Input(
439                'in_mr',
440                'Add/del row',
441                8, 1,
442                '(Template|Table|LDIF|[+-][0-9]+)',
443            ),
444            Select(
445                'in_oft', 'Type of input form',
446                1,
447                options=('Template', 'Table', 'LDIF'),
448                default='Template',
449            ),
450            AttributeType('in_at', 'Attribute type', web2ldapcnf.input_maxattrs),
451            AttributeType('in_avi', 'Value index', web2ldapcnf.input_maxattrs),
452            BytesInput(
453                'in_av', 'Attribute Value',
454                web2ldapcnf.input_maxfieldlen,
455                web2ldapcnf.input_maxattrs,
456                None
457            ),
458            LDIFTextArea('in_ldif', 'LDIF data'),
459            Select(
460                'in_ocf', 'Object class form mode', 1,
461                options=[
462                    ('tmpl', 'LDIF templates'),
463                    ('exp', 'Object class selection')
464                ],
465                default='tmpl',
466            ),
467        ])
468        return res
469
470
471class Web2LDAPFormAdd(Web2LDAPFormInput):
472    command = 'add'
473
474    def fields(self):
475        res = Web2LDAPFormInput.fields(self)
476        res.extend([
477            Input('add_rdn', 'RDN of new entry', 255, 1, '.*', size=50),
478            DistinguishedNameInput('add_clonedn', 'DN of template entry'),
479            Input(
480                'add_template', 'LDIF template name',
481                60,
482                web2ldapcnf.max_searchparams,
483                '.+',
484            ),
485            Input('add_basedn', 'Base DN of new entry', 1024, 1, '.*', size=50),
486        ])
487        return res
488
489
490class Web2LDAPFormModify(Web2LDAPFormInput):
491    command = 'modify'
492
493    def fields(self):
494        res = Web2LDAPFormInput.fields(self)
495        res.extend([
496            AttributeType(
497                'in_oldattrtypes',
498                'Old attribute types',
499                web2ldapcnf.input_maxattrs,
500            ),
501            AttributeType(
502                'in_roattroids',
503                'Read-only attribute types',
504                web2ldapcnf.input_maxattrs,
505            ),
506            Input(
507                'in_assertion',
508                'Assertion filter string',
509                2000,
510                1,
511                '.*',
512                required=False,
513            ),
514        ])
515        return res
516
517
518class Web2LDAPFormDds(Web2LDAPForm):
519    command = 'dds'
520
521    def fields(self):
522        res = Web2LDAPForm.fields(self)
523        res.extend([
524            Input(
525                'dds_renewttlnum',
526                'Request TTL number',
527                12, 1,
528                '[0-9]+',
529                default=None,
530            ),
531            Select(
532                'dds_renewttlfac',
533                'Request TTL factor',
534                1,
535                options=(
536                    ('1', 'seconds'),
537                    ('60', 'minutes'),
538                    ('3600', 'hours'),
539                    ('86400', 'days'),
540                ),
541                default='1'
542            ),
543        ])
544        return res
545
546
547class Web2LDAPFormBulkmod(Web2LDAPForm):
548    command = 'bulkmod'
549
550    def fields(self):
551        res = Web2LDAPForm.fields(self)
552        bulkmod_ctrl_options = [
553            (control_oid, OID_REG.get(control_oid, (control_oid,))[0])
554            for control_oid, control_spec in AVAILABLE_BOOLEAN_CONTROLS.items()
555            if (
556                '**all**' in control_spec[0]
557                or '**write**' in control_spec[0]
558                or 'modify' in control_spec[0]
559            )
560        ]
561        res.extend([
562            Input(
563                'bulkmod_submit',
564                'Search form submit button',
565                6, 1,
566                '(Next>>|<<Back|Apply|Cancel|[+-][0-9]+)',
567            ),
568            Select(
569                'bulkmod_ctrl',
570                'Extended controls',
571                len(bulkmod_ctrl_options),
572                options=bulkmod_ctrl_options,
573                default=None,
574                size=min(8, len(bulkmod_ctrl_options)),
575                multiSelect=1,
576            ),
577            Input(
578                'filterstr',
579                'Search filter string for searching entries to be deleted',
580                1200, 1,
581                '.*',
582            ),
583            Input(
584                'bulkmod_modrow',
585                'Add/del row',
586                8, 1,
587                '(Template|Table|LDIF|[+-][0-9]+)',
588            ),
589            AttributeType('bulkmod_at', 'Attribute type', web2ldapcnf.input_maxattrs),
590            Select(
591                'bulkmod_op',
592                'Modification type',
593                web2ldapcnf.input_maxattrs,
594                options=(
595                    ('', ''),
596                    (str(ldap0.MOD_ADD), 'add'),
597                    (str(ldap0.MOD_DELETE), 'delete'),
598                    (str(ldap0.MOD_REPLACE), 'replace'),
599                    (str(ldap0.MOD_INCREMENT), 'increment'),
600                ),
601                default=None,
602            ),
603            BytesInput(
604                'bulkmod_av', 'Attribute Value',
605                web2ldapcnf.input_maxfieldlen,
606                web2ldapcnf.input_maxattrs,
607                None,
608                size=30,
609            ),
610            DistinguishedNameInput('bulkmod_newsuperior', 'New superior DN'),
611            Checkbox('bulkmod_cp', 'Copy entries', 1, default='yes', checked=False),
612        ])
613        return res
614
615
616class Web2LDAPFormDelete(Web2LDAPForm):
617    command = 'delete'
618
619    def fields(self):
620        res = Web2LDAPForm.fields(self)
621        delete_ctrl_options = [
622            (control_oid, OID_REG.get(control_oid, (control_oid,))[0])
623            for control_oid, control_spec in AVAILABLE_BOOLEAN_CONTROLS.items()
624            if (
625                '**all**' in control_spec[0]
626                or '**write**' in control_spec[0]
627                or 'delete' in control_spec[0]
628            )
629        ]
630        delete_ctrl_options.append((CONTROL_TREEDELETE, 'Tree Deletion'))
631        res.extend([
632            Select(
633                'delete_confirm', 'Confirmation',
634                1,
635                options=('yes', 'no'),
636                default='no',
637            ),
638            Select(
639                'delete_ctrl',
640                'Extended controls',
641                len(delete_ctrl_options),
642                options=delete_ctrl_options,
643                default=None,
644                size=min(8, len(delete_ctrl_options)),
645                multiSelect=1,
646            ),
647            Input(
648                'filterstr',
649                'Search filter string for searching entries to be deleted',
650                1200, 1,
651                '.*',
652            ),
653            Input('delete_attr', 'Attribute to be deleted', 255, 100, '[\\w_;-]+'),
654        ])
655        return res
656
657
658class Web2LDAPFormRename(Web2LDAPForm):
659    command = 'rename'
660
661    def fields(self):
662        res = Web2LDAPForm.fields(self)
663        res.extend([
664            Input(
665                'rename_newrdn',
666                'New RDN',
667                255, 1,
668                RDN_PATTERN,
669                size=50,
670            ),
671            DistinguishedNameInput('rename_newsuperior', 'New superior DN'),
672            Checkbox('rename_delold', 'Delete old', 1, default='yes', checked=True),
673            Input(
674                'rename_newsupfilter',
675                'Filter string for searching new superior entry', 300, 1, '.*',
676                default='(|(objectClass=organization)(objectClass=organizationalUnit))',
677                size=50,
678            ),
679            DistinguishedNameInput(
680                'rename_searchroot',
681                'Search root under which to look for new superior entry.',
682            ),
683            Input(
684                'rename_supsearchurl',
685                'LDAP URL for searching new superior entry',
686                100, 1,
687                '.*',
688                size=30,
689            ),
690        ])
691        return res
692
693
694class Web2LDAPFormPasswd(Web2LDAPForm):
695    command = 'passwd'
696    passwd_actions = (
697        (
698            'passwdextop',
699            'Server-side',
700            'Password modify extended operation',
701        ),
702        (
703            'setuserpassword',
704            'Modify password attribute',
705            'Set the password attribute with modify operation'
706        ),
707    )
708
709    @staticmethod
710    def passwd_fields():
711        """
712        return list of Field instances needed for a password change input form
713        """
714        return [
715            Select(
716                'passwd_action', 'Password action', 1,
717                options=[
718                    (action, short_desc)
719                    for action, short_desc, _ in Web2LDAPFormPasswd.passwd_actions
720                ],
721                default='setuserpassword'
722            ),
723            DistinguishedNameInput('passwd_who', 'Password DN'),
724            Field('passwd_oldpasswd', 'Old password', 100, 1, '.*'),
725            Field('passwd_newpasswd', 'New password', 100, 2, '.*'),
726            Select(
727                'passwd_scheme', 'Password hash scheme', 1,
728                options=AVAIL_USERPASSWORD_SCHEMES.items(),
729                default=None,
730            ),
731            Checkbox(
732                'passwd_ntpasswordsync',
733                'Sync ntPassword for Samba',
734                1,
735                default='yes',
736                checked=True,
737            ),
738            Checkbox(
739                'passwd_settimesync',
740                'Sync password setting times',
741                1,
742                default='yes',
743                checked=True,
744            ),
745            Checkbox(
746                'passwd_forcechange',
747                'Force password change',
748                1,
749                default='yes',
750                checked=False,
751            ),
752            Checkbox(
753                'passwd_inform',
754                'Password change inform action',
755                1,
756                default="display_url",
757                checked=False,
758            ),
759        ]
760
761    def fields(self):
762        res = Web2LDAPForm.fields(self)
763        res.extend(self.passwd_fields())
764        return res
765
766
767class Web2LDAPFormRead(Web2LDAPForm):
768    command = 'read'
769
770    def fields(self):
771        res = Web2LDAPForm.fields(self)
772        res.extend([
773            Input(
774                'filterstr',
775                'Search filter string when reading single entry',
776                1200, 1,
777                '.*',
778            ),
779            Select(
780                'read_nocache', 'Force fresh read',
781                1,
782                options=['0', '1'],
783                default='0',
784            ),
785            Input('read_attr', 'Read attribute', 255, 100, '[\\w_;-]+'),
786            Input('read_attrindex', 'Read attribute', 255, 1, '[0-9]+'),
787            Input('read_attrmimetype', 'MIME type', 255, 1, '[\\w.-]+/[\\w.-]+'),
788            Select(
789                'read_output', 'Read output format',
790                1,
791                options=('table', 'vcard', 'template'),
792                default='template',
793            ),
794            SearchAttrs(),
795            Input('read_expandattr', 'Attributes to be read', 1000, 50, '[*+\\w,_;-]+'),
796        ])
797        return res
798
799
800class Web2LDAPFormGroupadm(Web2LDAPForm):
801    command = 'groupadm'
802
803    def fields(self):
804        res = Web2LDAPForm.fields(self)
805        res.extend([
806            DistinguishedNameInput('groupadm_searchroot', 'Group search root'),
807            Input('groupadm_name', 'Group name', 100, 1, '.*', size=30),
808            DistinguishedNameInput('groupadm_add', 'Add to group', 300),
809            DistinguishedNameInput('groupadm_remove', 'Remove from group', 300),
810            Select(
811                'groupadm_view',
812                'Group list view',
813                1,
814                options=(
815                    ('0', 'none of the'),
816                    ('1', 'only member'),
817                    ('2', 'all'),
818                ),
819                default='1',
820            ),
821        ])
822        return res
823
824
825class Web2LDAPFormLogin(Web2LDAPForm):
826    command = 'login'
827
828    def fields(self):
829        res = Web2LDAPForm.fields(self)
830        res.append(
831            DistinguishedNameInput('login_who', 'Bind DN')
832        )
833        return res
834
835
836class Web2LDAPFormLocate(Web2LDAPForm):
837    command = 'locate'
838
839    def fields(self):
840        res = Web2LDAPForm.fields(self)
841        res.append(
842            Input('locate_name', 'Location name', 500, 1, '.*', size=25)
843        )
844        return res
845
846
847class Web2LDAPFormOid(Web2LDAPForm):
848    command = 'oid'
849
850    def fields(self):
851        res = Web2LDAPForm.fields(self)
852        res.extend([
853            OIDInput('oid', 'OID'),
854            Select(
855                'oid_class',
856                'Schema element class',
857                1,
858                options=ldap0.schema.SCHEMA_ATTRS,
859                default='',
860            ),
861        ])
862        return res
863
864
865class Web2LDAPFormDit(Web2LDAPForm):
866    command = 'dit'
867
868
869class DistinguishedNameInput(Input):
870    """Input field class for LDAP DNs."""
871
872    def __init__(self, name='dn', text='DN', maxValues=1, required=False, default=''):
873        Input.__init__(
874            self, name, text, 1024, maxValues, None,
875            size=70, required=required, default=default
876        )
877
878    def _validate_format(self, value):
879        if value and not ldap0.dn.is_dn(value):
880            raise InvalidValueFormat(self.name, self.text, value)
881
882
883class LDIFTextArea(Textarea):
884    """A single multi-line input field for LDIF data"""
885
886    def __init__(
887            self,
888            name='in_ldif',
889            text='LDIF data',
890            required=False,
891            max_entries=1
892        ):
893        Textarea.__init__(
894            self,
895            name,
896            text,
897            web2ldapcnf.ldif_maxbytes,
898            1,
899            '^.*$',
900            required=required,
901        )
902        self._max_entries = max_entries
903
904    @property
905    def ldif_records(self):
906        if self.value:
907            return list(
908                ldap0.ldif.LDIFParser.frombuf(
909                    '\n'.join(self.value).encode(self.charset),
910                    ignored_attr_types=[],
911                    process_url_schemes=web2ldapcnf.ldif_url_schemes
912                ).parse(max_entries=self._max_entries)
913            )
914        return []
915
916
917class OIDInput(Input):
918
919    def __init__(self, name, text, default=None):
920        Input.__init__(
921            self, name, text,
922            512, 1, '[a-zA-Z0-9_.;*-]+',
923            default=default,
924            required=False,
925            size=30,
926        )
927
928
929class ObjectClassSelect(Select):
930    """Select field class for choosing the object class(es)"""
931
932    def __init__(
933            self,
934            name='in_oc',
935            text='Object classes',
936            options=None,
937            default=None,
938            required=False,
939            accesskey='',
940            size=12, # Size of displayed select field
941        ):
942        select_default = default or []
943        select_default.sort(key=str.lower)
944        additional_options = [
945            opt
946            for opt in options or []
947            if not opt in select_default
948        ]
949        additional_options.sort(key=str.lower)
950        select_options = select_default[:]
951        select_options.extend(additional_options)
952        Select.__init__(
953            self,
954            name, text,
955            maxValues=200,
956            required=required,
957            options=select_options,
958            default=select_default,
959            accesskey=accesskey,
960            size=size,
961            ignoreCase=1,
962            multiSelect=1
963        )
964        self.set_regex('[\\w]+')
965        self.maxLen = 200
966        # end of ObjectClassSelect()
967
968
969class ExportFormatSelect(Select):
970    """Select field class for choosing export format"""
971
972    def __init__(
973            self,
974            default='ldif1',
975            required=False,
976        ):
977        Select.__init__(
978            self,
979            'search_output',
980            'Export format',
981            1,
982            options=(
983                ('table', 'Table/template'),
984                ('raw', 'Raw DN list'),
985                ('print', 'Printable'),
986                ('ldif', 'LDIF (Umich)'),
987                ('ldif1', 'LDIFv1 (RFC2849)'),
988                ('csv', 'CSV'),
989                ('excel', 'Excel'),
990            ),
991            default=default,
992            required=required,
993            size=1,
994        )
995
996
997class AttributeType(Input):
998    """
999    Input field for an LDAP attribute type
1000    """
1001    def __init__(self, name, text, maxValues):
1002        Input.__init__(
1003            self,
1004            name,
1005            text,
1006            500,
1007            maxValues,
1008            ATTR_TYPE_PATTERN,
1009            required=False,
1010            size=30
1011        )
1012
1013
1014class InclOpAttrsCheckbox(Checkbox):
1015
1016    def __init__(self, default='yes', checked=False):
1017        Checkbox.__init__(
1018            self,
1019            'search_opattrs',
1020            'Request operational attributes',
1021            1,
1022            default=default,
1023            checked=checked
1024        )
1025
1026
1027class AuthMechSelect(Select):
1028    """Select field class for choosing the bind mech"""
1029
1030    supported_bind_mechs = {
1031        '': 'Simple Bind',
1032        'DIGEST-MD5': 'SASL Bind: DIGEST-MD5',
1033        'CRAM-MD5': 'SASL Bind: CRAM-MD5',
1034        'PLAIN': 'SASL Bind: PLAIN',
1035        'LOGIN': 'SASL Bind: LOGIN',
1036        'GSSAPI': 'SASL Bind: GSSAPI',
1037        'EXTERNAL': 'SASL Bind: EXTERNAL',
1038        'OTP': 'SASL Bind: OTP',
1039        'NTLM': 'SASL Bind: NTLM',
1040        'SCRAM-SHA-1': 'SASL Bind: SCRAM-SHA-1',
1041        'SCRAM-SHA-256': 'SASL Bind: SCRAM-SHA-256',
1042    }
1043
1044    def __init__(
1045            self,
1046            name='login_mech',
1047            text='Authentication mechanism',
1048            default=None,
1049            required=False,
1050            accesskey='',
1051            size=1,
1052        ):
1053        Select.__init__(
1054            self,
1055            name, text, maxValues=1,
1056            required=required,
1057            options=None,
1058            default=default or [],
1059            accesskey=accesskey,
1060            size=size,
1061            ignoreCase=0,
1062            multiSelect=0
1063        )
1064
1065    def set_options(self, options):
1066        options_dict = {}
1067        options_dict[''] = self.supported_bind_mechs['']
1068        for sasl_mech in options or self.supported_bind_mechs.keys():
1069            sasl_mech = sasl_mech.upper()
1070            if sasl_mech in self.supported_bind_mechs:
1071                options_dict[sasl_mech] = self.supported_bind_mechs[sasl_mech]
1072        Select.set_options(self, options_dict.items())
1073