web2ldap  1.7.7
About: web2ldap is a full-featured web-based LDAPv3 client.
  Fossies Dox: web2ldap-1.7.7.tar.gz  ("unofficial" and yet experimental doxygen-generated source code documentation)  

syntaxes.py
Go to the documentation of this file.
1# -*- coding: ascii -*-
2"""
3web2ldap.app.schema.syntaxes: classes for known attribute types
4
5web2ldap - a web-based LDAP Client,
6see https://www.web2ldap.de for details
7
8(C) 1998-2022 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 binascii
16import sys
17import os
18import re
19import imghdr
20import sndhdr
21import urllib.parse
22import uuid
23import datetime
24import time
25import json
26import inspect
27import warnings
28from typing import (
29 Callable,
30 Dict,
31 List,
32 Optional,
33 Pattern,
34 Sequence,
35 Tuple,
36)
37
38import iso3166
39
40try:
41 import defusedxml.ElementTree
42except ImportError:
43 DEFUSEDXML_AVAIL = False
44else:
45 DEFUSEDXML_AVAIL = True
46
47try:
48 import phonenumbers
49except ImportError:
50 PHONENUMBERS_AVAIL = False
51else:
52 PHONENUMBERS_AVAIL = True
53
54from collections import defaultdict
55from io import BytesIO
56
57# Detect Python Imaging Library (PIL)
58try:
59 from PIL import Image as PILImage
60except ImportError:
61 PIL_AVAIL = False
62else:
63 PIL_AVAIL = True
64 warnings.simplefilter('error', PILImage.DecompressionBombWarning)
65
66import ipaddress
67
68import ldap0
69import ldap0.ldapurl
70from ldap0.schema.models import AttributeType, ObjectClass, OBJECTCLASS_KIND_STR
71from ldap0.controls.deref import DereferenceControl
72from ldap0.dn import DNObj, is_dn
73from ldap0.res import SearchResultEntry
74from ldap0.schema.subentry import SubSchema
75
76import web2ldapcnf
77
78from ... import ETC_DIR
79from ...web import forms as web_forms
80from ...msbase import ascii_dump, chunks
81from ...utctime import repr2ts, ts2repr, strftimeiso8601
82from ...ldaputil.oidreg import OID_REG
83from ...log import logger
84from ... import cmp
85from . import schema_anchor
86from ..tmpl import get_variant_filename
87from ...utctime import strptime as utc_strptime
88from ..searchform import (
89 SEARCH_OPT_ATTR_EXISTS,
90 SEARCH_OPT_IS_EQUAL,
91 SEARCH_SCOPE_STR_ONELEVEL,
92)
93
94
96 """
97 syntax registry used to register plugin classes
98 """
99 __slots__ = (
100 'at2syntax',
101 'oid2syntax',
102 )
103
104 def __init__(self):
105 self.oid2syntax = ldap0.cidict.CIDict()
106 self.at2syntax = defaultdict(dict)
107
108 def reg_syntax(self, cls):
109 """
110 register a syntax classes for an OID
111 """
112 assert isinstance(cls.oid, str), ValueError(
113 'Expected %s.oid to be str, got %r' % (cls.__name__, cls.oid,)
114 )
115 logger.debug('Register syntax class %r with OID %r', cls.__name__, cls.oid)
116 # FIX ME!
117 # A better approach for unique syntax plugin class registration which
118 # allows overriding older registration is needed.
119 if cls.oid in self.oid2syntax and cls != self.oid2syntax[cls.oid]:
120 raise ValueError(
121 (
122 'Failed to register syntax class %s.%s with OID %s,'
123 ' already registered by %s.%s'
124 ) % (
125 cls.__module__,
126 cls.__name__,
127 repr(cls.oid),
128 self.oid2syntax[cls.oid].__module__,
129 self.oid2syntax[cls.oid].__name__,
130 )
131 )
132 self.oid2syntax[cls.oid] = cls
133
134 def reg_syntaxes(self, modulename):
135 """
136 register all syntax classes found in given module
137 """
138 logger.debug('Register syntax classes from module %r', modulename)
139 for _, cls in inspect.getmembers(sys.modules[modulename], inspect.isclass):
140 if issubclass(cls, LDAPSyntax) and hasattr(cls, 'oid'):
141 self.reg_syntax(cls)
142
143 def reg_at(self, syntax_oid: str, attr_types, structural_oc_oids=None):
144 """
145 register an attribute type (by OID) to explicitly use a certain LDAPSyntax class
146 """
147 logger.debug(
148 'Register syntax OID %s for %r / %r',
149 syntax_oid,
150 attr_types,
151 structural_oc_oids,
152 )
153 assert isinstance(syntax_oid, str), ValueError(
154 'Expected syntax_oid to be str, got %r' % (syntax_oid,)
155 )
156 structural_oc_oids = list(filter(None, map(str.strip, structural_oc_oids or []))) or [None]
157 for atype in attr_types:
158 atype = atype.strip()
159 for oc_oid in structural_oc_oids:
160 # FIX ME!
161 # A better approach for unique attribute type registration which
162 # allows overriding older registration is needed.
163 if atype in self.at2syntax and oc_oid in self.at2syntax[atype]:
164 logger.warning(
165 (
166 'Registering attribute type %r with syntax %r'
167 ' overrides existing registration with syntax %r'
168 ),
169 atype,
170 syntax_oid,
171 self.at2syntax[atype],
172 )
173 self.at2syntax[atype][oc_oid] = syntax_oid
174
175 def get_syntax(self, schema, attrtype_nameoroid, structural_oc):
176 """
177 returns LDAPSyntax class for given attribute type
178 """
179 assert isinstance(attrtype_nameoroid, str), ValueError(
180 'Expected attrtype_nameoroid to be str, got %r' % (attrtype_nameoroid,)
181 )
182 assert structural_oc is None or isinstance(structural_oc, str), ValueError(
183 'Expected structural_oc to be str or None, got %r' % (structural_oc,)
184 )
185 attrtype_oid = schema.get_oid(AttributeType, attrtype_nameoroid)
186 if structural_oc:
187 structural_oc_oid = schema.get_oid(ObjectClass, structural_oc)
188 else:
189 structural_oc_oid = None
190 syntax_oid = LDAPSyntax.oid
191 try:
192 syntax_oid = self.at2syntax[attrtype_oid][structural_oc_oid]
193 except KeyError:
194 try:
195 syntax_oid = self.at2syntax[attrtype_oid][None]
196 except KeyError:
197 attrtype_se = schema.get_inheritedobj(
198 AttributeType,
199 attrtype_oid,
200 ['syntax'],
201 )
202 if attrtype_se and attrtype_se.syntax:
203 syntax_oid = attrtype_se.syntax
204 try:
205 syntax_class = self.oid2syntax[syntax_oid]
206 except KeyError:
207 syntax_class = LDAPSyntax
208 return syntax_class
209
210 def get_at(self, app, dn, schema, attr_type, attr_value, entry=None):
211 """
212 returns LDAPSyntax instance fully initialized for given attribute
213 """
214 if entry:
215 structural_oc = entry.get_structural_oc()
216 else:
217 structural_oc = None
218 syntax_class = self.get_syntax(schema, attr_type, structural_oc)
219 attr_instance = syntax_class(app, dn, schema, attr_type, attr_value, entry)
220 return attr_instance
221
222 def check(self):
223 """
224 check whether attribute registry dict contains references by OID
225 for which no LDAPSyntax class are registered
226 """
227 logger.debug(
228 'Checking %d LDAPSyntax classes and %d attribute type mappings',
229 len(self.oid2syntax),
230 len(self.at2syntax),
231 )
232 for atype in self.at2syntax:
233 for object_class in self.at2syntax[atype]:
234 if self.at2syntax[atype][object_class] not in self.oid2syntax:
235 logger.warning('No LDAPSyntax registered for (%r, %r)', atype, object_class)
236
237
238####################################################################
239# Classes of known syntaxes
240####################################################################
241
242
243class LDAPSyntaxValueError(ValueError):
244 """
245 Exception raised in case a syntax check failed
246 """
247
248
249class LDAPSyntaxRegexNoMatch(LDAPSyntaxValueError):
250 """
251 Exception raised in case a regex pattern check failed
252 """
253
254
255class LDAPSyntax:
256 """
257 Base class for all LDAP syntax and attribute value plugin classes
258 """
259 __slots__ = (
260 '_app',
261 '_at',
262 '_av',
263 '_av_u',
264 '_dn',
265 '_entry',
266 '_schema',
267 )
268 oid: str = ''
269 desc: str = 'Any LDAP syntax'
270 input_size: int = 50
271 max_len: int = web2ldapcnf.input_maxfieldlen
272 max_values: int = web2ldapcnf.input_maxattrs
273 mime_type: str = 'application/octet-stream'
274 file_ext: str = 'bin'
275 editable: bool = True
276 pattern: Optional[Pattern[str]] = None
277 input_pattern: Optional[str] = None
278 search_sep: str = '<br>'
279 read_sep: str = '<br>'
280 field_sep: str = '<br>'
281 sani_funcs: Sequence[Callable] = (())
282 show_val_button: bool = True
283
285 self,
286 app,
287 dn: Optional[str],
288 schema: SubSchema,
289 attrType: Optional[str],
290 attr_value: Optional[bytes],
291 entry=None,
292 ):
293 if not entry:
294 entry = ldap0.schema.models.Entry(schema, dn, {})
295 assert isinstance(dn, str), \
296 TypeError("Argument 'dn' must be str, was %r" % (dn,))
297 assert isinstance(attrType, str) or attrType is None, \
298 TypeError("Argument 'attrType' must be str or None, was %r" % (attrType,))
299 assert isinstance(attr_value, bytes) or attr_value is None, \
300 TypeError("Argument 'attr_value' must be bytes or None, was %r" % (attr_value,))
301 assert entry is None or isinstance(entry, ldap0.schema.models.Entry), \
302 TypeError('entry must be ldaputil.schema.Entry, was %r' % (entry,))
303 self._at = attrType
304 self._av = attr_value
305 self._av_u = None
306 self._app = app
307 self._schema = schema
308 self._dn = dn
309 self._entry = entry
310
311 @property
312 def dn(self):
313 return DNObj.from_str(self._dn)
314
315 @property
316 def av_u(self):
317 if (self._av is not None and self._av_u is None):
318 self._av_u = self._app.ls.uc_decode(self._av)[0]
319 return self._av_u
320
321 def sanitize(self, attr_value: bytes) -> bytes:
322 """
323 Transforms the HTML form input field values into LDAP string
324 representations and returns raw binary string.
325
326 This is the inverse of LDAPSyntax.form_value().
327
328 When using this method one MUST NOT assume that the whole entry is
329 present.
330 """
331 for sani_func in self.sani_funcs:
332 attr_value = sani_func(attr_value)
333 return attr_value
334
335 def transmute(self, attr_values: List[bytes]) -> List[bytes]:
336 """
337 This method can be implemented to transmute attribute values and has
338 to handle LDAP string representations (raw binary strings).
339
340 This method has access to the whole entry after processing all input.
341
342 Implementors should be prepared that this method could be called
343 more than once. If there's nothing to change then simply return the
344 same value list.
345
346 Exceptions KeyError or IndexError are caught by the calling code to
347 re-iterate invoking this method.
348 """
349 return attr_values
350
351 def _validate(self, attr_value: bytes) -> bool:
352 """
353 check the syntax of attr_value
354
355 Implementors can overload this method to apply arbitrary syntax checks.
356 """
357 return True
358
359 def validate(self, attr_value: bytes):
360 if not attr_value:
361 return
362 if self.pattern and (self.pattern.match(attr_value.decode(self._app.ls.charset)) is None):
364 "Class %s: %r does not match pattern %r." % (
365 self.__class__.__name__,
366 attr_value,
367 self.pattern.pattern,
368 )
369 )
370 if not self._validate(attr_value):
372 "Class %s: %r does not comply to syntax (attr type %r)." % (
373 self.__class__.__name__,
374 attr_value,
375 self._at,
376 )
377 )
378 # end of validate()
379
380 def value_button(self, command, row, mode, link_text=None) -> str:
381 """
382 return HTML markup of [+] or [-] submit buttons for adding/removing
383 attribute values
384
385 row
386 row number in input table
387 mode
388 '+' or '-'
389 link_text
390 optionally override displayed link link_text
391 """
392 link_text = link_text or mode
393 if (
394 not self.show_val_button or
395 self.max_values <= 1 or
396 len(self._entry.get(self._at, [])) >= self.max_values
397 ):
398 return ''
399 se_obj = self._schema.get_obj(AttributeType, self._at)
400 if se_obj and se_obj.single_value:
401 return ''
402 return (
403 '<button'
404 ' formaction="%s#in_a_%s"'
405 ' type="submit"'
406 ' name="in_mr"'
407 ' value="%s%d">%s'
408 '</button>'
409 ) % (
410 self._app.form.action_url(command, self._app.sid),
411 self._app.form.s2d(self._at),
412 mode, row, link_text
413 )
414
415 def form_value(self) -> str:
416 """
417 Transform LDAP string representations to HTML form input field
418 values. Returns Unicode string to be encoded with the browser's
419 accepted charset.
420
421 This is the inverse of LDAPSyntax.sanitize().
422 """
423 try:
424 result = self.av_u or ''
425 except UnicodeDecodeError:
426 result = '!!!snipped because of UnicodeDecodeError!!!'
427 return result
428
429 def input_fields(self):
430 return (self.input_field(),)
431
432 def input_field(self) -> web_forms.Field:
433 input_field = web_forms.Input(
434 self._at,
435 ': '.join([self._at, self.desc]),
436 self.max_len,
437 self.max_values,
438 self.input_pattern,
439 default=None,
440 size=min(self.max_len, self.input_size),
441 )
442 input_field.charset = self._app.form.accept_charset
443 input_field.set_default(self.form_value())
444 return input_field
445
446 def display(self, vidx, links) -> str:
447 try:
448 res = self._app.form.s2d(self.av_u)
449 except UnicodeDecodeError:
450 res = self._app.form.s2d(repr(self._av))
451 return res
452
453
455 """
456 Plugin class for LDAP syntax 'Binary' (see RFC 2252)
457 """
458 oid: str = '1.3.6.1.4.1.1466.115.121.1.5'
459 desc: str = 'Binary'
460 editable: bool = False
461
462 def input_field(self) -> web_forms.Field:
463 field = web_forms.File(
464 self._at,
465 ': '.join([self._at, self.desc]),
466 self.max_len, self.max_values, None, default=self._av, size=50
467 )
468 field.mime_type = self.mime_type
469 return field
470
471 def display(self, vidx, links) -> str:
472 return '%d bytes | %s' % (
473 len(self._av),
474 self._app.anchor(
475 'read', 'View/Load',
476 [
477 ('dn', self._dn),
478 ('read_attr', self._at),
479 ('read_attrindex', str(vidx)),
480 ],
481 )
482 )
483
484
486 """
487 Plugin class for LDAP syntax 'Audio' (see RFC 2252)
488 """
489 oid: str = '1.3.6.1.4.1.1466.115.121.1.4'
490 desc: str = 'Audio'
491 mime_type: str = 'audio/basic'
492 file_ext: str = 'au'
493
494 def _validate(self, attr_value: bytes) -> bool:
495 with BytesIO(attr_value) as fileobj:
496 res = sndhdr.test_au(attr_value, fileobj)
497 return res is not None
498
499 def display(self, vidx, links) -> str:
500 mimetype = self.mime_type
501 return (
502 '<embed type="%s" autostart="false" '
503 'src="%s?dn=%s&amp;read_attr=%s&amp;read_attrindex=%d">'
504 '%d bytes of audio data (%s)'
505 ) % (
506 mimetype,
507 self._app.form.action_url('read', self._app.sid),
508 urllib.parse.quote(self._dn.encode(self._app.form.accept_charset)),
509 urllib.parse.quote(self._at),
510 vidx,
511 len(self._av),
512 mimetype
513 )
514
515
517 """
518 Plugin class for LDAP syntax 'Directory String'
519 (see https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.6)
520 """
521 oid: str = '1.3.6.1.4.1.1466.115.121.1.15'
522 desc: str = 'Directory String'
523 html_tmpl = '{av}'
524
525 def _validate(self, attr_value: bytes) -> bool:
526 try:
527 self._app.ls.uc_decode(attr_value)
528 except UnicodeDecodeError:
529 return False
530 return True
531
532 def display(self, vidx, links) -> str:
533 return self.html_tmpl.format(
534 av=self._app.form.s2d(self.av_u)
535 )
536
537
539 """
540 Plugin class for LDAP syntax 'DN'
541 (see https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.9)
542 """
543 oid: str = '1.3.6.1.4.1.1466.115.121.1.12'
544 desc: str = 'Distinguished Name'
545 isBindDN = False
546 hasSubordinates = False
547 ref_attrs: Optional[Sequence[Tuple[Optional[str], str, Optional[str], str]]] = None
548
549 def _validate(self, attr_value: bytes) -> bool:
550 return is_dn(self._app.ls.uc_decode(attr_value)[0])
551
553 res = []
554 if self._at.lower() != 'entrydn':
555 res.append(
556 self._app.anchor(
557 'read', 'Read',
558 [('dn', self.av_u)],
559 )
560 )
561 if self.hasSubordinates:
562 res.append(self._app.anchor(
563 'search', 'Down',
564 (
565 ('dn', self.av_u),
566 ('scope', SEARCH_SCOPE_STR_ONELEVEL),
567 ('filterstr', '(objectClass=*)'),
568 )
569 ))
570 if self.isBindDN:
571 ldap_url_obj = self._app.ls.ldap_url('', add_login=False)
572 res.append(
573 self._app.anchor(
574 'login',
575 'Bind as',
576 [
577 ('ldapurl', str(ldap_url_obj)),
578 ('dn', self._dn),
579 ('login_who', self.av_u),
580 ],
581 title='Connect and bind new session as\r\n%s' % (self.av_u)
582 ),
583 )
584 # If self.ref_attrs is not empty then add links for searching back-linking entries
585 for ref_attr_tuple in self.ref_attrs or tuple():
586 try:
587 ref_attr, ref_text, ref_dn, ref_oc, ref_title = ref_attr_tuple
588 except ValueError:
589 ref_oc = None
590 ref_attr, ref_text, ref_dn, ref_title = ref_attr_tuple
591 ref_attr = ref_attr or self._at
592 if ref_attr not in self._schema.name2oid[AttributeType]:
593 continue
594 ref_dn = ref_dn or self._dn
595 ref_title = ref_title or 'Search %s entries referencing entry %s in attribute %s' % (
596 ref_oc, self.av_u, ref_attr,
597 )
598 res.append(self._app.anchor(
599 'search', self._app.form.s2d(ref_text),
600 (
601 ('dn', ref_dn),
602 ('search_root', str(self._app.naming_context)),
603 ('searchform_mode', 'adv'),
604 ('search_attr', 'objectClass'),
605 (
606 'search_option',
607 {
608 True: SEARCH_OPT_ATTR_EXISTS,
609 False: SEARCH_OPT_IS_EQUAL,
610 }[ref_oc is None]
611 ),
612 ('search_string', ref_oc or ''),
613 ('search_attr', ref_attr),
614 ('search_option', SEARCH_OPT_IS_EQUAL),
615 ('search_string', self.av_u),
616 ),
617 title=ref_title,
618 ))
619 return res
620
621 def display(self, vidx, links) -> str:
622 res = [self._app.form.s2d(self.av_u or '- World -')]
623 if links:
624 res.extend(self._additional_links())
625 return web2ldapcnf.command_link_separator.join(res)
626
627
629 """
630 Plugin class for DNs probably usable as bind-DN
631 """
632 oid: str = 'BindDN-oid'
633 desc: str = 'A Distinguished Name used to bind to a directory'
634 isBindDN = True
635
636
638 """
639 Plugin class for DNs used for authorization
640 """
641 oid: str = 'AuthzDN-oid'
642 desc: str = 'Authz Distinguished Name'
643
644 def display(self, vidx, links) -> str:
645 result = DistinguishedName.display(self, vidx, links)
646 if links:
647 simple_display_str = DistinguishedName.display(
648 self,
649 vidx,
650 links=False,
651 )
652 whoami_display_str = self._app.display_authz_dn(who=self.av_u)
653 if whoami_display_str != simple_display_str:
654 result = '<br>'.join((whoami_display_str, result))
655 return result
656
657
659 """
660 Plugin class for LDAP syntax 'Name and Optional UID'
661 (see https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.21)
662 """
663 oid: str = '1.3.6.1.4.1.1466.115.121.1.34'
664 desc: str = 'Name And Optional UID'
665
666 @staticmethod
667 def _split_dn_and_uid(val: str) -> Tuple[str, Optional[str]]:
668 try:
669 sep_ind = val.rindex('#')
670 except ValueError:
671 dn = val
672 uid = None
673 else:
674 dn = val[0:sep_ind]
675 uid = val[sep_ind+1:]
676 return dn, uid
677
678 def _validate(self, attr_value: bytes) -> bool:
679 dn, _ = self._split_dn_and_uid(self._app.ls.uc_decode(attr_value)[0])
680 return is_dn(dn)
681
682 def display(self, vidx, links) -> str:
683 value = self.av_u.split('#')
684 dn_str = self._app.display_dn(
685 self.av_u,
686 links=links,
687 )
688 if len(value) == 1 or not value[1]:
689 return dn_str
690 return web2ldapcnf.command_link_separator.join([
691 self._app.form.s2d(value[1]),
692 dn_str,
693 ])
694
695
697 """
698 Plugin class for LDAP syntax 'Bit String'
699 (see https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.2)
700 """
701 oid: str = '1.3.6.1.4.1.1466.115.121.1.6'
702 desc: str = 'Bit String'
703 pattern = re.compile("^'[01]+'B$")
704
705
707 """
708 Plugin class for LDAP syntax 'IA5 String'
709 (see https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.15)
710 """
711 oid: str = '1.3.6.1.4.1.1466.115.121.1.26'
712 desc: str = 'IA5 String'
713
714 def _validate(self, attr_value: bytes) -> bool:
715 try:
716 _ = attr_value.decode('ascii').encode('ascii')
717 except UnicodeError:
718 return False
719 return True
720
721
723 """
724 Plugin class for LDAP syntax 'Generalized Time'
725 (see https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.13)
726 """
727 oid: str = '1.3.6.1.4.1.1466.115.121.1.24'
728 desc: str = 'Generalized Time'
729 input_size: int = 24
730 max_len: int = 24
731 pattern = re.compile(r'^([0-9]){12,14}((\.|,)[0-9]+)*(Z|(\+|-)[0-9]{4})$')
732 timeDefault = None
733 notBefore = None
734 notAfter = None
735 form_value_fmt = '%Y-%m-%dT%H:%M:%SZ'
736 dtFormats = (
737 '%Y%m%d%H%M%SZ',
738 '%Y-%m-%dT%H:%M:%SZ',
739 '%Y-%m-%dT%H:%MZ',
740 '%Y-%m-%dT%H:%M:%S+00:00',
741 '%Y-%m-%dT%H:%M:%S-00:00',
742 '%Y-%m-%d %H:%M:%SZ',
743 '%Y-%m-%d %H:%MZ',
744 '%Y-%m-%d %H:%M',
745 '%Y-%m-%d %H:%M:%S+00:00',
746 '%Y-%m-%d %H:%M:%S-00:00',
747 '%d.%m.%YT%H:%M:%SZ',
748 '%d.%m.%YT%H:%MZ',
749 '%d.%m.%YT%H:%M:%S+00:00',
750 '%d.%m.%YT%H:%M:%S-00:00',
751 '%d.%m.%Y %H:%M:%SZ',
752 '%d.%m.%Y %H:%MZ',
753 '%d.%m.%Y %H:%M',
754 '%d.%m.%Y %H:%M:%S+00:00',
755 '%d.%m.%Y %H:%M:%S-00:00',
756 )
757 acceptable_formats = (
758 '%Y-%m-%d',
759 '%d.%m.%Y',
760 '%m/%d/%Y',
761 )
762 dt_display_format = (
763 '<time datetime="%Y-%m-%dT%H:%M:%SZ">'
764 '%A (%W. week) %Y-%m-%d %H:%M:%S+00:00'
765 '</time>'
766 )
767
768 def _validate(self, attr_value: bytes) -> bool:
769 try:
770 d_t = utc_strptime(attr_value)
771 except ValueError:
772 return False
773 return (
774 (self.notBefore is None or self.notBefore <= d_t)
775 and (self.notAfter is None or self.notAfter >= d_t)
776 )
777
778 def form_value(self) -> str:
779 if not self._av:
780 return ''
781 try:
782 d_t = datetime.datetime.strptime(self.av_u, r'%Y%m%d%H%M%SZ')
783 except ValueError:
784 result = IA5String.form_value(self)
785 else:
786 result = str(datetime.datetime.strftime(d_t, self.form_value_fmt))
787 return result
788
789 def sanitize(self, attr_value: bytes) -> bytes:
790 av_u = self._app.ls.uc_decode(attr_value.strip().upper())[0]
791 # Special cases first
792 if av_u in {'N', 'NOW', '0'}:
793 return datetime.datetime.strftime(
794 datetime.datetime.utcnow(),
795 r'%Y%m%d%H%M%SZ',
796 ).encode('ascii')
797 # a single integer value is interpreted as seconds relative to now
798 try:
799 float_val = float(av_u)
800 except ValueError:
801 pass
802 else:
803 return datetime.datetime.strftime(
804 datetime.datetime.utcnow()+datetime.timedelta(seconds=float_val),
805 r'%Y%m%d%H%M%SZ',
806 ).encode('ascii')
807 if self.timeDefault:
808 date_format = r'%Y%m%d' + self.timeDefault + 'Z'
809 if av_u in ('T', 'TODAY'):
810 return datetime.datetime.strftime(
811 datetime.datetime.utcnow(),
812 date_format,
813 ).encode('ascii')
814 if av_u in ('Y', 'YESTERDAY'):
815 return datetime.datetime.strftime(
816 datetime.datetime.today()-datetime.timedelta(days=1),
817 date_format,
818 ).encode('ascii')
819 if av_u in ('T', 'TOMORROW'):
820 return datetime.datetime.strftime(
821 datetime.datetime.today()+datetime.timedelta(days=1),
822 date_format,
823 ).encode('ascii')
824 # Try to parse various datetime syntaxes
825 for time_format in self.dtFormats:
826 try:
827 d_t = datetime.datetime.strptime(av_u, time_format)
828 except ValueError:
829 result = None
830 else:
831 result = datetime.datetime.strftime(d_t, r'%Y%m%d%H%M%SZ')
832 break
833 if result is None:
834 if self.timeDefault:
835 for time_format in self.acceptable_formats or []:
836 try:
837 d_t = datetime.datetime.strptime(av_u, time_format)
838 except ValueError:
839 result = None
840 else:
841 result = datetime.datetime.strftime(d_t, r'%Y%m%d'+self.timeDefault+'Z')
842 break
843 else:
844 result = av_u
845 if result is None:
846 return IA5String.sanitize(self, attr_value)
847 return result.encode('ascii')
848 # end of GeneralizedTime.sanitize()
849
850 def display(self, vidx, links) -> str:
851 try:
852 dt_utc = utc_strptime(self.av_u)
853 except ValueError:
854 return IA5String.display(self, vidx, links)
855 try:
856 dt_utc_str = dt_utc.strftime(self.dt_display_format)
857 except ValueError:
858 return IA5String.display(self, vidx, links)
859 if not links:
860 return dt_utc_str
861 current_time = datetime.datetime.utcnow()
862 time_span = (current_time - dt_utc).total_seconds()
863 return '{dt_utc} ({av})<br>{timespan_disp} {timespan_comment}'.format(
864 dt_utc=dt_utc_str,
865 av=self._app.form.s2d(self.av_u),
866 timespan_disp=self._app.form.s2d(
867 ts2repr(Timespan.time_divisors, ' ', abs(time_span))
868 ),
869 timespan_comment={
870 1: 'ago',
871 0: '',
872 -1: 'ahead',
873 }[cmp(time_span, 0)]
874 )
875
876
878 """
879 Plugin class for attributes indicating start of a period
880 """
881 oid: str = 'NotBefore-oid'
882 desc: str = 'A not-before timestamp by default starting at 00:00:00'
883 timeDefault = '000000'
884
885
887 """
888 Plugin class for attributes indicating end of a period
889 """
890 oid: str = 'NotAfter-oid'
891 desc: str = 'A not-after timestamp by default ending at 23:59:59'
892 timeDefault = '235959'
893
894
896 """
897 Plugin class for LDAP syntax 'UTC Time'
898 (see https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.34)
899 """
900 oid: str = '1.3.6.1.4.1.1466.115.121.1.53'
901 desc: str = 'UTC Time'
902
903
905 """
906 Plugin class for strings terminated with null-byte
907 """
908 oid: str = 'NullTerminatedDirectoryString-oid'
909 desc: str = 'Directory String terminated by null-byte'
910
911 def sanitize(self, attr_value: bytes) -> bytes:
912 return attr_value + b'\x00'
913
914 def _validate(self, attr_value: bytes) -> bool:
915 return attr_value.endswith(b'\x00')
916
917 def form_value(self) -> str:
918 return self._app.ls.uc_decode((self._av or b'\x00')[:-1])[0]
919
920 def display(self, vidx, links) -> str:
921 return self._app.form.s2d(
922 self._app.ls.uc_decode((self._av or b'\x00')[:-1])[0]
923 )
924
925
927 """
928 Plugin class for LDAP syntax 'Other Mailbox'
929 (see https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.27)
930 """
931 oid: str = '1.3.6.1.4.1.1466.115.121.1.39'
932 desc: str = 'Other Mailbox'
933 charset = 'ascii'
934
935
937 """
938 Plugin class for LDAP syntax 'Integer'
939 (see https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.16)
940 """
941 oid: str = '1.3.6.1.4.1.1466.115.121.1.27'
942 desc: str = 'Integer'
943 input_size: int = 12
944 min_value = None
945 max_value = None
946
947 def __init__(self, app, dn: str, schema, attrType: str, attr_value: bytes, entry=None):
948 IA5String.__init__(self, app, dn, schema, attrType, attr_value, entry)
949 if self.max_value is not None:
950 self.max_len = len(str(self.max_value))
951
952 def _maxlen(self, fval):
953 min_value_len = max_value_len = fval_len = 0
954 if self.min_value is not None:
955 min_value_len = len(str(self.min_value))
956 if self.max_value is not None:
957 max_value_len = len(str(self.max_value))
958 if fval is not None:
959 fval_len = len(fval.encode(self._app.ls.charset))
960 return max(self.input_size, fval_len, min_value_len, max_value_len)
961
962 def _validate(self, attr_value: bytes) -> bool:
963 try:
964 val = int(attr_value)
965 except ValueError:
966 return False
967 min_value, max_value = self.min_value, self.max_value
968 return (
969 (min_value is None or val >= min_value) and
970 (max_value is None or val <= max_value)
971 )
972
973 def sanitize(self, attr_value: bytes) -> bytes:
974 try:
975 return str(int(attr_value)).encode('ascii')
976 except ValueError:
977 return attr_value
978
979 def input_field(self) -> web_forms.Field:
980 fval = self.form_value()
981 max_len = self._maxlen(fval)
982 input_field = web_forms.Input(
983 self._at,
984 ': '.join([self._at, self.desc]),
985 max_len,
986 self.max_values,
987 self.input_pattern,
988 default=fval,
989 size=min(self.input_size, max_len),
990 )
991 input_field.input_type = 'number'
992 return input_field
993
994
996 """
997 Plugin class for string representation of IPv4 or IPv6 host address
998 """
999 oid: str = 'IPHostAddress-oid'
1000 desc: str = 'string representation of IPv4 or IPv6 address'
1001 # Class in module ipaddr which parses address/network values
1002 addr_class = None
1003 sani_funcs = (
1004 bytes.strip,
1005 )
1006
1007 def _validate(self, attr_value: bytes) -> bool:
1008 try:
1009 addr = ipaddress.ip_address(attr_value.decode('ascii'))
1010 except ValueError:
1011 return False
1012 return self.addr_class is None or isinstance(addr, self.addr_class)
1013
1014
1016 """
1017 Plugin class for string representation of IPv4 host address
1018 """
1019 oid: str = 'IPv4HostAddress-oid'
1020 desc: str = 'string representation of IPv4 address'
1021 addr_class = ipaddress.IPv4Address
1022
1023
1025 """
1026 Plugin class for string representation of IPv6 host address
1027 """
1028 oid: str = 'IPv6HostAddress-oid'
1029 desc: str = 'string representation of IPv6 address'
1030 addr_class = ipaddress.IPv6Address
1031
1032
1034 """
1035 Plugin class for string representation of IPv4 or IPv6 network address
1036 """
1037 oid: str = 'IPNetworkAddress-oid'
1038 desc: str = 'string representation of IPv4 or IPv6 network address/mask'
1039
1040 def _validate(self, attr_value: bytes) -> bool:
1041 try:
1042 addr = ipaddress.ip_network(attr_value.decode('ascii'), strict=False)
1043 except ValueError:
1044 return False
1045 return self.addr_class is None or isinstance(addr, self.addr_class)
1046
1047
1049 """
1050 Plugin class for string representation of IPv4 network address
1051 """
1052 oid: str = 'IPv4NetworkAddress-oid'
1053 desc: str = 'string representation of IPv4 network address/mask'
1054 addr_class = ipaddress.IPv4Network
1055
1056
1058 """
1059 Plugin class for string representation of IPv6 network address
1060 """
1061 oid: str = 'IPv6NetworkAddress-oid'
1062 desc: str = 'string representation of IPv6 network address/mask'
1063 addr_class = ipaddress.IPv6Network
1064
1065
1067 """
1068 Plugin class for service port number (see /etc/services)
1069 """
1070 oid: str = 'IPServicePortNumber-oid'
1071 desc: str = 'Port number for an UDP- or TCP-based service'
1072 min_value = 0
1073 max_value = 65535
1074
1075
1077 """
1078 Plugin class for IEEEE MAC addresses of network devices
1079 """
1080 oid: str = 'MacAddress-oid'
1081 desc: str = 'MAC address in hex-colon notation'
1082 min_len: int = 17
1083 max_len: int = 17
1084 pattern = re.compile(r'^([0-9a-f]{2}\:){5}[0-9a-f]{2}$')
1085
1086 def sanitize(self, attr_value: bytes) -> bytes:
1087 attr_value = attr_value.translate(None, b'.-: ').lower().strip()
1088 if len(attr_value) == 12:
1089 return b':'.join([attr_value[i*2:i*2+2] for i in range(6)])
1090 return attr_value
1091
1092
1094 """
1095 Plugin class for Uniform Resource Identifiers (URIs, see RFC 2079)
1096 """
1097 oid: str = 'Uri-OID'
1098 desc: str = 'URI'
1099 pattern = re.compile(r'^(ftp|http|https|news|snews|ldap|ldaps|mailto):(|//)[^ ]*')
1100 sani_funcs = (
1101 bytes.strip,
1102 )
1103
1104 def display(self, vidx, links) -> str:
1105 attr_value = self.av_u
1106 try:
1107 url, label = attr_value.split(' ', 1)
1108 except ValueError:
1109 url, label = attr_value, attr_value
1110 display_url = ''
1111 else:
1112 display_url = ' (%s)' % (url)
1113 if ldap0.ldapurl.is_ldapurl(url):
1114 return '<a href="%s?%s">%s%s</a>' % (
1115 self._app.form.script_name,
1116 self._app.form.s2d(url),
1117 self._app.form.s2d(label),
1118 self._app.form.s2d(display_url),
1119 )
1120 if url.lower().find('javascript:') >= 0:
1121 return '<code>%s</code>' % (
1122 DirectoryString.display(self, vidx=False, links=False)
1123 )
1124 return '<a href="%s?%s">%s%s</a>' % (
1125 self._app.form.action_url('urlredirect', self._app.sid),
1126 self._app.form.s2d(url),
1127 self._app.form.s2d(label),
1128 self._app.form.s2d(display_url),
1129 )
1130
1131
1133 """
1134 Plugin base class for attributes containing image data.
1135 """
1136 oid: str = 'Image-OID'
1137 desc: str = 'Image base class'
1138 mime_type: str = 'application/octet-stream'
1139 file_ext: str = 'bin'
1140 imageFormat = None
1141 inline_maxlen = 630 # max. number of bytes to use data: URI instead of external URL
1142
1143 def _validate(self, attr_value: bytes) -> bool:
1144 return imghdr.what(None, attr_value) == self.imageFormat.lower()
1145
1146 def sanitize(self, attr_value: bytes) -> bytes:
1147 if not self._validate_validate(attr_value) and PIL_AVAIL:
1148 try:
1149 with BytesIO(attr_value) as imgfile:
1150 img = PILImage.open(imgfile)
1151 imgfile.seek(0)
1152 img.save(imgfile, self.imageFormat)
1153 attr_value = imgfile.getvalue()
1154 except Exception as err:
1155 logger.warning(
1156 'Error converting image data (%d bytes) to %s: %r',
1157 len(attr_value),
1158 self.imageFormat,
1159 err,
1160 )
1161 return attr_value
1162
1163 def display(self, vidx, links) -> str:
1164 maxwidth, maxheight = 100, 150
1165 width, height = None, None
1166 size_attr_html = ''
1167 if PIL_AVAIL:
1168 try:
1169 with BytesIO(self._av) as imgfile:
1170 img = PILImage.open(imgfile)
1171 except IOError:
1172 pass
1173 else:
1174 width, height = img.size
1175 if width > maxwidth:
1176 size_attr_html = 'width="%d" height="%d"' % (
1177 maxwidth,
1178 int(float(maxwidth)/width*height),
1179 )
1180 elif height > maxheight:
1181 size_attr_html = 'width="%d" height="%d"' % (
1182 int(float(maxheight)/height*width),
1183 maxheight,
1184 )
1185 else:
1186 size_attr_html = 'width="%d" height="%d"' % (width, height)
1187 attr_value_len = len(self._av)
1188 img_link = '%s?dn=%s&amp;read_attr=%s&amp;read_attrindex=%d' % (
1189 self._app.form.action_url('read', self._app.sid),
1190 urllib.parse.quote(self._dn),
1191 urllib.parse.quote(self._at),
1192 vidx,
1193 )
1194 if attr_value_len <= self.inline_maxlen:
1195 return (
1196 '<a href="%s">'
1197 '<img src="data:%s;base64,\n%s" alt="%d bytes of image data" %s>'
1198 '</a>'
1199 ) % (
1200 img_link,
1201 self.mime_type,
1202 self._av.encode('base64'),
1203 attr_value_len,
1204 size_attr_html,
1205 )
1206 return '<a href="%s"><img src="%s" alt="%d bytes of image data" %s></a>' % (
1207 img_link,
1208 img_link,
1209 attr_value_len,
1210 size_attr_html,
1211 )
1212
1213
1215 """
1216 Plugin class for LDAP syntax 'JPEG'
1217 (see https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.17)
1218 """
1219 oid: str = '1.3.6.1.4.1.1466.115.121.1.28'
1220 desc: str = 'JPEG image'
1221 mime_type: str = 'image/jpeg'
1222 file_ext: str = 'jpg'
1223 imageFormat = 'JPEG'
1224
1225
1227 """
1228 Plugin class for LDAP syntax 'Fax'
1229 (see https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.12)
1230 """
1231 oid: str = '1.3.6.1.4.1.1466.115.121.1.23'
1232 desc: str = 'Photo (G3 fax)'
1233 mime_type: str = 'image/g3fax'
1234 file_ext: str = 'tif'
1235
1236
1238 """
1239 Plugin class for LDAP syntax 'OID'
1240 (see https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.26)
1241 """
1242 oid: str = '1.3.6.1.4.1.1466.115.121.1.38'
1243 desc: str = 'OID'
1244 pattern = re.compile(r'^([a-zA-Z]+[a-zA-Z0-9;-]*|[0-2]?\.([0-9]+\.)*[0-9]+)$')
1245 no_val_button_attrs = frozenset((
1246 'objectclass',
1247 'structuralobjectclass',
1248 '2.5.4.0',
1249 '2.5.21.9',
1250 ))
1251
1252 def value_button(self, command, row, mode, link_text=None) -> str:
1253 if self._at.lower() in self.no_val_button_attrs:
1254 return ''
1255 return IA5String.value_button(self, command, row, mode, link_text=link_text)
1256
1257 def sanitize(self, attr_value: bytes) -> bytes:
1258 return attr_value.strip()
1259
1260 def display(self, vidx, links) -> str:
1261 try:
1262 name, description, reference = OID_REG[self.av_u]
1263 except (KeyError, ValueError):
1264 try:
1265 se_obj = self._schema.get_obj(
1266 ObjectClass,
1267 self.av_u,
1268 raise_keyerror=1,
1269 )
1270 except KeyError:
1271 try:
1272 se_obj = self._schema.get_obj(
1273 AttributeType,
1274 self.av_u,
1275 raise_keyerror=1,
1276 )
1277 except KeyError:
1278 return IA5String.display(self, vidx, links)
1279 return schema_anchor(
1280 self._app,
1281 self.av_u,
1282 AttributeType,
1283 name_template='{name}\n{anchor}',
1284 link_text='&raquo;',
1285 )
1286 if self._at.lower() == 'structuralobjectclass':
1287 name_template = '{name}\n{anchor}'
1288 else:
1289 name_template = '{name}\n (%s){anchor}' % (OBJECTCLASS_KIND_STR[se_obj.kind],)
1290 # objectClass attribute is displayed with different function
1291 return schema_anchor(
1292 self._app,
1293 self.av_u,
1294 ObjectClass,
1295 name_template=name_template,
1296 link_text='&raquo;',
1297 )
1298 return '<strong>%s</strong> (%s):<br>%s (see %s)' % (
1299 self._app.form.s2d(name),
1300 IA5String.display(self, vidx, links),
1301 self._app.form.s2d(description),
1302 self._app.form.s2d(reference)
1303 )
1304
1305
1307 """
1308 Plugin class for attributes containing LDAP URLs
1309 """
1310 oid: str = 'LDAPUrl-oid'
1311 desc: str = 'LDAP URL'
1312
1313 def _command_ldap_url(self, ldap_url):
1314 return ldap_url
1315
1316 def display(self, vidx, links) -> str:
1317 try:
1318 if links:
1319 linksstr = self._app.ldap_url_anchor(
1320 self._command_ldap_url(self.av_u),
1321 )
1322 else:
1323 linksstr = ''
1324 except ValueError:
1325 return '<strong>Not a valid LDAP URL:</strong> %s' % (
1326 self._app.form.s2d(repr(self._av))
1327 )
1328 return '<table><tr><td>%s</td><td><a href="%s">%s</a></td></tr></table>' % (
1329 linksstr,
1330 self._app.form.s2d(self.av_u),
1331 self._app.form.s2d(self.av_u)
1332 )
1333
1334
1336 """
1337 Plugin class for LDAP syntax 'Octet String'
1338 (see https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.25)
1339 """
1340 oid: str = '1.3.6.1.4.1.1466.115.121.1.40'
1341 desc: str = 'Octet String'
1342 editable: bool = True
1343 min_input_rows = 1 # minimum number of rows for input field
1344 max_input_rows = 15 # maximum number of rows for in input field
1345 bytes_split = 16
1346
1347 def sanitize(self, attr_value: bytes) -> bytes:
1348 attr_value = attr_value.translate(None, b': ,\r\n')
1349 try:
1350 res = binascii.unhexlify(attr_value)
1351 except binascii.Error:
1352 res = attr_value
1353 return res
1354
1355 def display(self, vidx, links) -> str:
1356 lines = [
1357 (
1358 '<tr>'
1359 '<td><code>%0.6X</code></td>'
1360 '<td><code>%s</code></td>'
1361 '<td><code>%s</code></td>'
1362 '</tr>'
1363 ) % (
1364 i*self.bytes_split,
1365 ':'.join(c[j:j+1].hex().upper() for j in range(len(c))),
1366 self._app.form.s2d(ascii_dump(c), 'ascii'),
1367 )
1368 for i, c in enumerate(chunks(self._av, self.bytes_split))
1369 ]
1370 return '\n<table class="HexDump">\n%s\n</table>\n' % ('\n'.join(lines))
1371
1372 def form_value(self) -> str:
1373 hex_av = (self._av or b'').hex().upper()
1374 hex_range = range(0, len(hex_av), 2)
1375 return str('\r\n'.join(
1376 chunks(
1377 ':'.join([hex_av[i:i+2] for i in hex_range]),
1378 self.bytes_split*3
1379 )
1380 ))
1381
1382 def input_field(self) -> web_forms.Field:
1383 fval = self.form_valueform_value()
1384 return web_forms.Textarea(
1385 self._at,
1386 ': '.join([self._at, self.desc]),
1387 10000, 1,
1388 None,
1389 default=fval,
1390 rows=max(self.min_input_rows, min(self.max_input_rows, fval.count('\r\n'))),
1391 cols=49
1392 )
1393
1394
1396 """
1397 Plugin base class for multi-line text.
1398 """
1399 oid: str = 'MultilineText-oid'
1400 desc: str = 'Multiple lines of text'
1401 pattern = re.compile('^.*$', re.S+re.M)
1402 lineSep = b'\r\n'
1403 mime_type: str = 'text/plain'
1404 cols = 66
1405 min_input_rows = 1 # minimum number of rows for input field
1406 max_input_rows = 30 # maximum number of rows for in input field
1407
1408 def _split_lines(self, value):
1409 if self.lineSep:
1410 return value.split(self.lineSep)
1411 return [value]
1412
1413 def sanitize(self, attr_value: bytes) -> bytes:
1414 return attr_value.replace(
1415 b'\r', b''
1416 ).replace(
1417 b'\n', self.lineSep
1418 )
1419
1420 def display(self, vidx, links) -> str:
1421 return '<br>'.join([
1422 self._app.form.s2d(self._app.ls.uc_decode(line_b)[0])
1423 for line_b in self._split_lines(self._av)
1424 ])
1425
1426 def form_value(self) -> str:
1427 splitted_lines = [
1428 self._app.ls.uc_decode(line_b)[0]
1429 for line_b in self._split_lines(self._av or b'')
1430 ]
1431 return '\r\n'.join(splitted_lines)
1432
1433 def input_field(self) -> web_forms.Field:
1434 fval = self.form_valueform_value()
1435 return web_forms.Textarea(
1436 self._at,
1437 ': '.join([self._at, self.desc]),
1438 self.max_len, self.max_values,
1439 None,
1440 default=fval,
1441 rows=max(self.min_input_rows, min(self.max_input_rows, fval.count('\r\n'))),
1442 cols=self.cols
1443 )
1444
1445
1447 """
1448 Plugin base class for multi-line text displayed with mono-spaced font,
1449 e.g. program code, XML, JSON etc.
1450 """
1451 oid: str = 'PreformattedMultilineText-oid'
1452 cols = 66
1453 tab_identiation = '&nbsp;&nbsp;&nbsp;&nbsp;'
1454
1455 def display(self, vidx, links) -> str:
1456 lines = [
1457 self._app.form.s2d(
1458 self._app.ls.uc_decode(line_b)[0],
1459 self.tab_identiation,
1460 )
1461 for line_b in self._split_lines(self._av)
1462 ]
1463 return '<code>%s</code>' % '<br>'.join(lines)
1464
1465
1467 """
1468 Plugin class for LDAP syntax 'Postal Address'
1469 (see https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.28)
1470 """
1471 oid: str = '1.3.6.1.4.1.1466.115.121.1.41'
1472 desc: str = 'Postal Address'
1473 lineSep = b' $ '
1474 cols = 40
1475
1476 def _split_lines(self, value):
1477 return [
1478 v.strip()
1479 for v in value.split(self.lineSeplineSep.strip())
1480 ]
1481
1482 def sanitize(self, attr_value: bytes) -> bytes:
1483 return attr_value.replace(b'\r', b'').replace(b'\n', self.lineSeplineSep)
1484
1485
1487 """
1488 Plugin class for LDAP syntax 'Printable String'
1489 (see https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.29)
1490 """
1491 oid: str = '1.3.6.1.4.1.1466.115.121.1.44'
1492 desc: str = 'Printable String'
1493 pattern = re.compile("^[a-zA-Z0-9'()+,.=/:? -]*$")
1494 charset = 'ascii'
1495
1496
1498 """
1499 Plugin class for LDAP syntax 'Numeric String'
1500 (see https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.23)
1501 """
1502 oid: str = '1.3.6.1.4.1.1466.115.121.1.36'
1503 desc: str = 'Numeric String'
1504 pattern = re.compile('^[ 0-9]+$')
1505
1506
1508 """
1509 Plugin class for LDAP syntax 'Enhanced Guide'
1510 (see https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.10)
1511 """
1512 oid: str = '1.3.6.1.4.1.1466.115.121.1.21'
1513 desc: str = 'Enhanced Search Guide'
1514
1515
1517 """
1518 Plugin class for LDAP syntax 'Search Guide'
1519 (see https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.14)
1520 """
1521 oid: str = '1.3.6.1.4.1.1466.115.121.1.25'
1522 desc: str = 'Search Guide'
1523
1524
1526 """
1527 Plugin class for LDAP syntax ''
1528 (see https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.31)
1529 """
1530 oid: str = '1.3.6.1.4.1.1466.115.121.1.50'
1531 desc: str = 'Telephone Number'
1532 pattern = re.compile('^[0-9+x(). /-]+$')
1533
1534 def sanitize(self, attr_value: bytes) -> bytes:
1535 if PHONENUMBERS_AVAIL:
1536 try:
1537 attr_value = phonenumbers.format_number(
1538 phonenumbers.parse(
1539 attr_value.decode('ascii'),
1540 region=(
1541 self._entry['c'][0].decode('ascii')
1542 if 'c' in self._entry
1543 else None
1544 ),
1545 ),
1546 phonenumbers.PhoneNumberFormat.INTERNATIONAL,
1547 ).encode('ascii')
1548 except (
1549 UnicodeDecodeError,
1550 ValueError,
1551 phonenumbers.phonenumberutil.NumberParseException,
1552 ):
1553 attr_value = PrintableString.sanitize(self, attr_value)
1554 else:
1555 attr_value = PrintableString.sanitize(self, attr_value)
1556 return attr_value
1557
1558
1560 """
1561 Plugin class for LDAP syntax 'Facsimile Telephone Number'
1562 (see https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.11)
1563 """
1564 oid: str = '1.3.6.1.4.1.1466.115.121.1.22'
1565 desc: str = 'Facsimile Number'
1566 pattern = re.compile(
1567 r'^[0-9+x(). /-]+'
1568 r'(\$'
1569 r'(twoDimensional|fineResolution|unlimitedLength|b4Length|a3Width|b4Width|uncompressed)'
1570 r')*$'
1571 )
1572
1573
1575 """
1576 Plugin class for LDAP syntax 'Telex Number'
1577 (see https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.33)
1578 """
1579 oid: str = '1.3.6.1.4.1.1466.115.121.1.52'
1580 desc: str = 'Telex Number'
1581 pattern = re.compile("^[a-zA-Z0-9'()+,.=/:?$ -]*$")
1582
1583
1585 """
1586 Plugin class for LDAP syntax 'Teletex Terminal Identifier'
1587 (see https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.32)
1588 """
1589 oid: str = '1.3.6.1.4.1.1466.115.121.1.51'
1590 desc: str = 'Teletex Terminal Identifier'
1591
1592
1594 oid: str = 'ObjectGUID-oid'
1595 desc: str = 'Object GUID'
1596 charset = 'ascii'
1597
1598 def display(self, vidx, links) -> str:
1599 objectguid_str = ''.join([
1600 '%02X' % ord(c)
1601 for c in self._av
1602 ])
1603 return ldap0.ldapurl.LDAPUrl(
1604 ldapUrl=self._app.ls.uri,
1605 dn='GUID=%s' % (objectguid_str),
1606 who=None, cred=None
1607 ).htmlHREF(
1608 hrefText=objectguid_str,
1609 hrefTarget=None
1610 )
1611
1612
1614 """
1615 Plugin base class for a date without(!) time component.
1616 """
1617 oid: str = 'Date-oid'
1618 desc: str = 'Date in syntax specified by class attribute storage_format'
1619 max_len: int = 10
1620 storage_format = '%Y-%m-%d'
1621 acceptable_formats = (
1622 '%Y-%m-%d',
1623 '%d.%m.%Y',
1624 '%m/%d/%Y',
1625 )
1626
1627 def _validate(self, attr_value: bytes) -> bool:
1628 try:
1629 datetime.datetime.strptime(
1630 self._app.ls.uc_decode(attr_value)[0],
1631 self.storage_format
1632 )
1633 except (UnicodeDecodeError, ValueError):
1634 return False
1635 return True
1636
1637 def sanitize(self, attr_value: bytes) -> bytes:
1638 av_u = attr_value.strip().decode(self._app.ls.charset)
1639 result = attr_value
1640 for time_format in self.acceptable_formats:
1641 try:
1642 time_tuple = datetime.datetime.strptime(av_u, time_format)
1643 except ValueError:
1644 pass
1645 else:
1646 result = datetime.datetime.strftime(time_tuple, self.storage_format).encode('ascii')
1647 break
1648 return result # sanitize()
1649
1650
1652 """
1653 Plugin class for a date using syntax YYYYMMDD typically
1654 using LDAP syntax Numstring.
1655 """
1656 oid: str = 'NumstringDate-oid'
1657 desc: str = 'Date in syntax YYYYMMDD'
1658 pattern = re.compile('^[0-9]{4}[0-1][0-9][0-3][0-9]$')
1659 storage_format = '%Y%m%d'
1660
1661
1663 """
1664 Plugin class for a date using syntax YYYY-MM-DD (see ISO 8601).
1665 """
1666 oid: str = 'ISO8601Date-oid'
1667 desc: str = 'Date in syntax YYYY-MM-DD (see ISO 8601)'
1668 pattern = re.compile('^[0-9]{4}-[0-1][0-9]-[0-3][0-9]$')
1669 storage_format = '%Y-%m-%d'
1670
1671
1673 """
1674 Plugin class for date of birth syntax YYYY-MM-DD (see ISO 8601).
1675
1676 Displays the age based at current time.
1677 """
1678 oid: str = 'DateOfBirth-oid'
1679 desc: str = 'Date of birth: syntax YYYY-MM-DD (see ISO 8601)'
1680
1681 @staticmethod
1682 def _age(birth_dt):
1683 birth_date = datetime.date(
1684 year=birth_dt.year,
1685 month=birth_dt.month,
1686 day=birth_dt.day,
1687 )
1688 current_date = datetime.date.today()
1689 age = current_date.year - birth_date.year
1690 if birth_date.month > current_date.month or \
1691 (birth_date.month == current_date.month and birth_date.day > current_date.day):
1692 age = age - 1
1693 return age
1694
1695 def _validate(self, attr_value: bytes) -> bool:
1696 try:
1697 birth_dt = datetime.datetime.strptime(
1698 self._app.ls.uc_decode(attr_value)[0],
1700 )
1701 except ValueError:
1702 return False
1703 return self._age(birth_dt) >= 0
1704
1705 def display(self, vidx, links) -> str:
1706 raw_date = ISO8601Date.display(self, vidx, links)
1707 try:
1708 birth_dt = datetime.datetime.strptime(self.av_u, self.storage_formatstorage_format)
1709 except ValueError:
1710 return raw_date
1711 return '%s (%s years old)' % (raw_date, self._age(birth_dt))
1712
1713
1715 """
1716 Plugin class for seconds since epoch (1970-01-01 00:00:00).
1717 """
1718 oid: str = 'SecondsSinceEpoch-oid'
1719 desc: str = 'Seconds since epoch (1970-01-01 00:00:00)'
1720 min_value = 0
1721
1722 def display(self, vidx, links) -> str:
1723 int_str = Integer.display(self, vidx, links)
1724 try:
1725 return '%s (%s)' % (
1726 strftimeiso8601(time.gmtime(float(self._av))),
1727 int_str,
1728 )
1729 except ValueError:
1730 return int_str
1731
1732
1734 """
1735 Plugin class for days since epoch (1970-01-01).
1736 """
1737 oid: str = 'DaysSinceEpoch-oid'
1738 desc: str = 'Days since epoch (1970-01-01)'
1739 min_value = 0
1740
1741 def display(self, vidx, links) -> str:
1742 int_str = Integer.display(self, vidx, links)
1743 try:
1744 return '%s (%s)' % (
1745 strftimeiso8601(time.gmtime(float(self._av)*86400)),
1746 int_str,
1747 )
1748 except ValueError:
1749 return int_str
1750
1751
1753 oid: str = 'Timespan-oid'
1754 desc: str = 'Time span in seconds'
1755 input_size: int = LDAPSyntax.input_size
1756 min_value = 0
1757 time_divisors = (
1758 ('weeks', 604800),
1759 ('days', 86400),
1760 ('hours', 3600),
1761 ('mins', 60),
1762 ('secs', 1),
1763 )
1764 sep = ','
1765
1766 def sanitize(self, attr_value: bytes) -> bytes:
1767 if not attr_value:
1768 return attr_value
1769 try:
1770 result = repr2ts(
1771 self.time_divisors,
1772 self.sep,
1773 attr_value.decode('ascii')
1774 ).encode('ascii')
1775 except ValueError:
1776 result = Integer.sanitize(self, attr_value)
1777 return result
1778
1779 def form_value(self) -> str:
1780 if not self._av:
1781 return ''
1782 try:
1783 result = ts2repr(self.time_divisors, self.sep, self._av)
1784 except ValueError:
1785 result = Integer.form_value(self)
1786 return result
1787
1788 def input_field(self) -> web_forms.Field:
1789 return IA5String.input_field(self)
1790
1791 def display(self, vidx, links) -> str:
1792 try:
1793 result = self._app.form.s2d('%s (%s)' % (
1794 ts2repr(self.time_divisors, self.sep, self.av_u),
1795 Integer.display(self, vidx, links)
1796 ))
1797 except ValueError:
1798 result = Integer.display(self, vidx, links)
1799 return result
1800
1801
1803 """
1804 Base class for dictionary based select lists which
1805 should not be used directly
1806 """
1807 oid: str = 'SelectList-oid'
1808 attr_value_dict: Dict[str, str] = {} # Mapping attribute value to attribute description
1809 input_fallback: bool = True # Fallback to normal input field if attr_value_dict is empty
1810 desc_sep: str = ' '
1811 tag_tmpl: Dict[bool, str] = {
1812 False: '{attr_text}: {attr_value}',
1813 True: '<span title="{attr_title}">{attr_text}:{sep}{attr_value}</span>',
1814 }
1815
1816 def get_attr_value_dict(self) -> Dict[str, str]:
1817 # Enable empty value in any case
1818 attr_value_dict: Dict[str, str] = {'': '-/-'}
1819 attr_value_dict.update(self.attr_value_dict)
1820 return attr_value_dict
1821
1823 # First generate a set of all other currently available attribute values
1824 fval = DirectoryString.form_value(self)
1825 # Initialize a dictionary with all options
1826 vdict = self.get_attr_value_dict()
1827 # Remove other existing values from the options dict
1828 for val in self._entry.get(self._at, []):
1829 val = self._app.ls.uc_decode(val)[0]
1830 if val != fval:
1831 try:
1832 del vdict[val]
1833 except KeyError:
1834 pass
1835 # Add the current attribute value if needed
1836 if fval not in vdict:
1837 vdict[fval] = fval
1838 # Finally return the sorted option list
1839 result = []
1840 for key, val in vdict.items():
1841 if isinstance(val, str):
1842 result.append((key, val, None))
1843 elif isinstance(val, tuple):
1844 result.append((key, val[0], val[1]))
1845 return sorted(
1846 result,
1847 key=lambda x: x[1].lower(),
1848 )
1849
1850 def _validate(self, attr_value: bytes) -> bool:
1851 attr_value_dict: Dict[str, str] = self.get_attr_value_dict()
1852 return self._app.ls.uc_decode(attr_value)[0] in attr_value_dict
1853
1854 def display(self, vidx, links) -> str:
1855 attr_value_str = DirectoryString.display(self, vidx, links)
1856 attr_value_dict: Dict[str, str] = self.get_attr_value_dict()
1857 try:
1858 attr_value_desc = attr_value_dict[self.av_u]
1859 except KeyError:
1860 return attr_value_str
1861 try:
1862 attr_text, attr_title = attr_value_desc
1863 except ValueError:
1864 attr_text, attr_title = attr_value_desc, None
1865 if attr_text == attr_value_str:
1866 return attr_value_str
1867 return self.tag_tmpl[bool(attr_title)].format(
1868 attr_value=attr_value_str,
1869 sep=self.desc_sep,
1870 attr_text=self._app.form.s2d(attr_text),
1871 attr_title=self._app.form.s2d(attr_title or '')
1872 )
1873
1874 def input_field(self) -> web_forms.Field:
1875 attr_value_dict: Dict[str, str] = self.get_attr_value_dict()
1876 if self.input_fallback and \
1877 (not attr_value_dict or not list(filter(None, attr_value_dict.keys()))):
1878 return DirectoryString.input_field(self)
1879 field = web_forms.Select(
1880 self._at,
1881 ': '.join([self._at, self.desc]),
1882 1,
1883 options=self._sorted_select_options(),
1884 default=self.form_value(),
1885 required=0
1886 )
1887 field.charset = self._app.form.accept_charset
1888 return field
1889
1890
1892 """
1893 Plugin base class for attribute value select lists of LDAP syntax DirectoryString
1894 constructed and validated by reading a properties file.
1895 """
1896 oid: str = 'PropertiesSelectList-oid'
1897 properties_pathname: Optional[str] = None
1898 properties_charset: str = 'utf-8'
1899 properties_delimiter: str = '='
1900
1901 def get_attr_value_dict(self) -> Dict[str, str]:
1902 attr_value_dict: Dict[str, str] = SelectList.get_attr_value_dict(self)
1903 real_path_name = get_variant_filename(
1904 self.properties_pathname,
1905 self._app.form.accept_language
1906 )
1907 with open(real_path_name, 'rb') as prop_file:
1908 for line in prop_file.readlines():
1909 line = line.decode(self.properties_charset).strip()
1910 if line and not line.startswith('#'):
1911 key, value = line.split(self.properties_delimiter, 1)
1912 attr_value_dict[key.strip()] = value.strip()
1913 return attr_value_dict
1914 # end of get_attr_value_dict()
1915
1916
1918 """
1919 Plugin base class for attribute value select lists of LDAP syntax DirectoryString
1920 constructed and validated by internal LDAP search.
1921 """
1922 oid: str = 'DynamicValueSelectList-oid'
1923 ldap_url: Optional[str] = None
1924 value_prefix: str = ''
1925 value_suffix: str = ''
1926 ignored_errors = (
1927 ldap0.NO_SUCH_OBJECT,
1928 ldap0.SIZELIMIT_EXCEEDED,
1929 ldap0.TIMELIMIT_EXCEEDED,
1930 ldap0.PARTIAL_RESULTS,
1931 ldap0.INSUFFICIENT_ACCESS,
1932 ldap0.CONSTRAINT_VIOLATION,
1933 ldap0.REFERRAL,
1934 )
1935
1936 def __init__(self, app, dn: str, schema, attrType: str, attr_value: bytes, entry=None):
1937 self.lu_obj = ldap0.ldapurl.LDAPUrl(self.ldap_url)
1938 self.min_len = len(self.value_prefix)+len(self.value_suffix)
1939 SelectList.__init__(self, app, dn, schema, attrType, attr_value, entry)
1940
1941 def _filterstr(self):
1942 return self.lu_obj.filterstr or '(objectClass=*)'
1943
1944 def _search_ref(self, attr_value: str):
1945 attr_value = attr_value[len(self.value_prefix):-len(self.value_suffix) or None]
1946 search_filter = '(&%s(%s=%s))' % (
1947 self._filterstr(),
1948 self.lu_obj.attrs[0],
1949 attr_value,
1950 )
1951 try:
1952 ldap_result = self._app.ls.l.search_s(
1953 self._search_root(),
1954 self.lu_obj.scope,
1955 search_filter,
1956 attrlist=self.lu_obj.attrs,
1957 sizelimit=2,
1958 )
1959 except (
1960 ldap0.NO_SUCH_OBJECT,
1961 ldap0.CONSTRAINT_VIOLATION,
1962 ldap0.INSUFFICIENT_ACCESS,
1963 ldap0.REFERRAL,
1964 ldap0.SIZELIMIT_EXCEEDED,
1965 ldap0.TIMELIMIT_EXCEEDED,
1966 ):
1967 return None
1968 # Filter out LDAP referrals
1969 ldap_result = [
1970 (sre.dn_s, sre.entry_s)
1971 for sre in ldap_result
1972 if isinstance(sre, SearchResultEntry)
1973 ]
1974 if ldap_result and len(ldap_result) == 1:
1975 return ldap_result[0]
1976 return None
1977
1978 def _validate(self, attr_value: bytes) -> bool:
1979 av_u = self._app.ls.uc_decode(attr_value)[0]
1980 if (
1981 not av_u.startswith(self.value_prefix) or
1982 not av_u.endswith(self.value_suffix) or
1983 len(av_u) < self.min_len or
1984 (self.max_len is not None and len(av_u) > self.max_len)
1985 ):
1986 return False
1987 return self._search_ref(av_u) is not None
1988
1989 def display(self, vidx, links) -> str:
1990 if links and self.lu_obj.attrs:
1991 ref_result = self._search_ref(self.av_u)
1992 if ref_result:
1993 ref_dn, ref_entry = ref_result
1994 try:
1995 attr_value_desc = ref_entry[self.lu_obj.attrs[1]][0]
1996 except (KeyError, IndexError):
1997 display_text, link_html = '', ''
1998 else:
1999 if self.lu_obj.attrs[0].lower() == self.lu_obj.attrs[1].lower():
2000 display_text = ''
2001 else:
2002 display_text = self._app.form.s2d(attr_value_desc+':')
2003 if links:
2004 link_html = self._app.anchor(
2005 'read', '&raquo;',
2006 [('dn', ref_dn)],
2007 )
2008 else:
2009 link_html = ''
2010 else:
2011 display_text, link_html = '', ''
2012 else:
2013 display_text, link_html = '', ''
2014 return ' '.join((
2015 display_text,
2016 DirectoryString.display(self, vidx, links),
2017 link_html,
2018 ))
2019
2020 def _search_root(self) -> str:
2021 ldap_url_dn = self.lu_obj.dn
2022 if ldap_url_dn == '_':
2023 result_dn = str(self._app.naming_context)
2024 elif ldap_url_dn == '.':
2025 result_dn = self._dn
2026 elif ldap_url_dn == '..':
2027 result_dn = str(self.dn.parent())
2028 elif ldap_url_dn.endswith(',_'):
2029 result_dn = ','.join((ldap_url_dn[:-2], str(self._app.naming_context)))
2030 elif ldap_url_dn.endswith(',.'):
2031 result_dn = ','.join((ldap_url_dn[:-2], self._dn))
2032 elif ldap_url_dn.endswith(',..'):
2033 result_dn = ','.join((ldap_url_dn[:-3], str(self.dn.parent())))
2034 else:
2035 result_dn = ldap_url_dn
2036 if result_dn.endswith(','):
2037 result_dn = result_dn[:-1]
2038 return result_dn
2039 # end of _search_root()
2040
2041 def get_attr_value_dict(self) -> Dict[str, str]:
2042 attr_value_dict: Dict[str, str] = SelectList.get_attr_value_dict(self)
2043 if self.lu_obj.hostport:
2044 raise ValueError(
2045 'Connecting to other server not supported! hostport attribute was %r' % (
2046 self.lu_obj.hostport
2047 )
2048 )
2049 search_scope = self.lu_obj.scope or ldap0.SCOPE_BASE
2050 search_attrs = (self.lu_obj.attrs or []) + ['description', 'info']
2051 # Use the existing LDAP connection as current user
2052 try:
2053 ldap_result = self._app.ls.l.search_s(
2054 self._search_root(),
2055 search_scope,
2056 filterstr=self._filterstr(),
2057 attrlist=search_attrs,
2058 )
2059 except self.ignored_errors:
2060 return {}
2061 if search_scope == ldap0.SCOPE_BASE:
2062 # When reading a single entry we build the map from a single multi-valued attribute
2063 assert len(self.lu_obj.attrs or []) == 1, ValueError(
2064 'attrlist in ldap_url must be of length 1 if scope is base, got %r' % (
2065 self.lu_obj.attrs,
2066 )
2067 )
2068 list_attr = self.lu_obj.attrs[0]
2069 attr_values_u = [
2070 ''.join((
2071 self.value_prefix,
2072 attr_value,
2073 self.value_suffix,
2074 ))
2075 for attr_value in ldap_result[0].entry_s[list_attr]
2076 ]
2077 attr_value_dict: Dict[str, str] = {
2078 u: u
2079 for u in attr_values_u
2080 }
2081 else:
2082 if not self.lu_obj.attrs:
2083 option_value_map, option_text_map = (None, None)
2084 elif len(self.lu_obj.attrs) == 1:
2085 option_value_map, option_text_map = (None, self.lu_obj.attrs[0])
2086 elif len(self.lu_obj.attrs) >= 2:
2087 option_value_map, option_text_map = self.lu_obj.attrs[:2]
2088 for sre in ldap_result:
2089 # Check whether it's a real search result (skip search continuations)
2090 if not isinstance(sre, SearchResultEntry):
2091 continue
2092 sre.entry_s[None] = [sre.dn_s]
2093 try:
2094 option_value = ''.join((
2095 self.value_prefix,
2096 sre.entry_s[option_value_map][0],
2097 self.value_suffix,
2098 ))
2099 except KeyError:
2100 pass
2101 else:
2102 try:
2103 option_text = sre.entry_s[option_text_map][0]
2104 except KeyError:
2105 option_text = option_value
2106 option_title = sre.entry_s.get('description', sre.entry_s.get('info', ['']))[0]
2107 if option_title:
2108 attr_value_dict[option_value] = (option_text, option_title)
2109 else:
2110 attr_value_dict[option_value] = option_text
2111 return attr_value_dict
2112 # end of get_attr_value_dict()
2113
2114
2116 """
2117 Plugin base class for attribute value select lists of LDAP syntax DN
2118 constructed and validated by internal LDAP search.
2119 """
2120 oid: str = 'DynamicDNSelectList-oid'
2121
2122 def _get_ref_entry(self, dn: str, attrlist=None) -> dict:
2123 try:
2124 sre = self._app.ls.l.read_s(
2125 dn,
2126 attrlist=attrlist or self.lu_obj.attrs,
2127 filterstr=self._filterstr(),
2128 )
2129 except (
2130 ldap0.NO_SUCH_OBJECT,
2131 ldap0.CONSTRAINT_VIOLATION,
2132 ldap0.INSUFFICIENT_ACCESS,
2133 ldap0.INVALID_DN_SYNTAX,
2134 ldap0.REFERRAL,
2135 ):
2136 return None
2137 if sre is None:
2138 return None
2139 return sre.entry_s
2140
2141 def _validate(self, attr_value: bytes) -> bool:
2142 return SelectList._validate(self, attr_value)
2143
2144 def display(self, vidx, links) -> str:
2145 if links and self.lu_obj.attrs:
2146 ref_entry = self._get_ref_entry(self.av_u) or {}
2147 try:
2148 attr_value_desc = ref_entry[self.lu_obj.attrs[0]][0]
2149 except (KeyError, IndexError):
2150 display_text = ''
2151 else:
2152 display_text = self._app.form.s2d(attr_value_desc+': ')
2153 else:
2154 display_text = ''
2155 return self.desc_sep.join((
2156 display_text,
2157 DistinguishedName.display(self, vidx, links)
2158 ))
2159
2160
2162 """
2163 Plugin base class for attribute value select lists of LDAP syntax DN
2164 constructed and validated by internal LDAP search.
2165
2166 Same as DynamicDNSelectList except that Dereference extended control is used.
2167 """
2168 oid: str = 'DerefDynamicDNSelectList-oid'
2169
2170 def _get_ref_entry(self, dn: str, attrlist=None) -> dict:
2171 deref_crtl = DereferenceControl(
2172 True,
2173 {self._at: self.lu_obj.attrs or ['entryDN']}
2174 )
2175 try:
2176 ldap_result = self._app.ls.l.search_s(
2177 self._dn,
2178 ldap0.SCOPE_BASE,
2179 filterstr='(objectClass=*)',
2180 attrlist=['1.1'],
2181 req_ctrls=[deref_crtl],
2182 )[0]
2183 except (
2184 ldap0.NO_SUCH_OBJECT,
2185 ldap0.CONSTRAINT_VIOLATION,
2186 ldap0.INSUFFICIENT_ACCESS,
2187 ldap0.INVALID_DN_SYNTAX,
2188 ldap0.REFERRAL,
2189 ):
2190 return None
2191 if ldap_result is None or not ldap_result.ctrls:
2192 return None
2193 for ref in ldap_result.ctrls[0].derefRes[self._at]:
2194 if ref.dn_s == dn:
2195 return ref.entry_s
2196 return None
2197
2198
2200 """
2201 Plugin class for LDAP syntax 'Boolean'
2202 (see https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.3)
2203 """
2204 oid: str = '1.3.6.1.4.1.1466.115.121.1.7'
2205 desc: str = 'Boolean'
2206 attr_value_dict: Dict[str, str] = {
2207 'TRUE': 'TRUE',
2208 'FALSE': 'FALSE',
2209 }
2210
2211 def get_attr_value_dict(self) -> Dict[str, str]:
2212 attr_value_dict: Dict[str, str] = SelectList.get_attr_value_dict(self)
2213 if self._av and self._av.lower() == self._av:
2214 for key, val in attr_value_dict.items():
2215 del attr_value_dict[key]
2216 attr_value_dict[key.lower()] = val.lower()
2217 return attr_value_dict
2218
2219 def _validate(self, attr_value: bytes) -> bool:
2220 if not self._av and attr_value.lower() == attr_value:
2221 return SelectList._validate(self, attr_value.upper())
2222 return SelectList._validate(self, attr_value)
2223
2224 def display(self, vidx, links) -> str:
2225 return IA5String.display(self, vidx, links)
2226
2227
2229 """
2230 Plugin class for LDAP syntax 'Country String'
2231 (see https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.4)
2232 """
2233 oid: str = '1.3.6.1.4.1.1466.115.121.1.11'
2234 desc: str = 'Two letter country string as listed in ISO 3166-2'
2235 sani_funcs = (
2236 bytes.strip,
2237 )
2238
2239 def get_attr_value_dict(self) -> Dict[str, str]:
2240 # Enable empty value in any case
2241 attr_value_dict: Dict[str, str] = {'': '-/-'}
2242 attr_value_dict.update({
2243 alpha2: cty.name for alpha2, cty in iso3166.countries_by_alpha2.items()
2244 })
2245 return attr_value_dict
2246
2247
2249 """
2250 Plugin class for LDAP syntax 'Delivery Method'
2251 (see https://datatracker.ietf.org/doc/html/rfc4517#section-3.3.5)
2252 """
2253 oid: str = '1.3.6.1.4.1.1466.115.121.1.14'
2254 desc: str = 'Delivery Method'
2255 pdm = '(any|mhs|physical|telex|teletex|g3fax|g4fax|ia5|videotex|telephone)'
2256 pattern = re.compile('^%s[ $]*%s$' % (pdm, pdm))
2257
2258
2260 """
2261 Plugin class for attributes with Integer syntax where the integer
2262 value is interpreted as binary flags
2263 """
2264 oid: str = 'BitArrayInteger-oid'
2265 flag_desc_table: Sequence[Tuple[str, int]] = tuple()
2266 true_false_desc: Dict[bool, str] = {
2267 False: '-',
2268 True: '+',
2269 }
2270
2271 def __init__(self, app, dn: str, schema, attrType: str, attr_value: bytes, entry=None):
2272 Integer.__init__(self, app, dn, schema, attrType, attr_value, entry)
2273 self.flag_desc2int = dict(self.flag_desc_table)
2275 j: i
2276 for i, j in self.flag_desc_table
2277 }
2278 self.max_valuemax_value = sum([j for i, j in self.flag_desc_table])
2279 self.min_input_rowsmin_input_rows = self.max_input_rowsmax_input_rows = max(len(self.flag_desc_table), 1)
2280
2281 def sanitize(self, attr_value: bytes) -> bytes:
2282 try:
2283 av_u = attr_value.decode('ascii')
2284 except UnicodeDecodeError:
2285 return attr_value
2286 try:
2287 result = int(av_u)
2288 except ValueError:
2289 result = 0
2290 for row in av_u.split('\n'):
2291 row = row.strip()
2292 try:
2293 flag_set, flag_desc = row[0:1], row[1:]
2294 except IndexError:
2295 pass
2296 else:
2297 if flag_set == '+':
2298 try:
2299 result = result | self.flag_desc2int[flag_desc]
2300 except KeyError:
2301 pass
2302 return str(result).encode('ascii')
2303
2304 def form_value(self) -> str:
2305 attr_value_int = int(self.av_u or 0)
2306 flag_lines = [
2307 ''.join((
2308 self.true_false_desc[int((attr_value_int & flag_int) > 0)],
2309 flag_desc
2310 ))
2311 for flag_desc, flag_int in self.flag_desc_table
2312 ]
2313 return '\r\n'.join(flag_lines)
2314
2315 def input_field(self) -> web_forms.Field:
2316 fval = self.form_valueform_valueform_value()
2317 return web_forms.Textarea(
2318 self._at,
2319 ': '.join([self._at, self.desc]),
2320 self.max_len, self.max_values,
2321 None,
2322 default=fval,
2323 rows=max(self.min_input_rowsmin_input_rows, min(self.max_input_rowsmax_input_rows, fval.count('\n'))),
2324 cols=max([len(desc) for desc, _ in self.flag_desc_table])+1
2325 )
2326
2327 def display(self, vidx, links) -> str:
2328 av_i = int(self._av)
2329 return (
2330 '%s<br>'
2331 '<table summary="Flags">'
2332 '<tr><th>Property flag</th><th>Value</th><th>Status</th></tr>'
2333 '%s'
2334 '</table>'
2335 ) % (
2336 Integer.display(self, vidx, links),
2337 '\n'.join([
2338 '<tr><td>%s</td><td>%s</td><td>%s</td></tr>' % (
2339 self._app.form.s2d(desc),
2340 hex(flag_value),
2341 {False: '-', True: 'on'}[(av_i & flag_value) > 0]
2342 )
2343 for desc, flag_value in self.flag_desc_table
2344 ])
2345 )
2346
2347
2349 """
2350 Generic String Encoding Rules (GSER) for ASN.1 Types (see RFC 3641)
2351 """
2352 oid: str = 'GSER-oid'
2353 desc: str = 'GSER syntax (see RFC 3641)'
2354
2355
2357 """
2358 Plugin class for Universally Unique IDentifier (UUID), see RFC 4122
2359 """
2360 oid: str = '1.3.6.1.1.16.1'
2361 desc: str = 'UUID'
2362 pattern = re.compile(
2363 '^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$'
2364 )
2365
2366 def sanitize(self, attr_value: bytes) -> bytes:
2367 try:
2368 return str(uuid.UUID(attr_value.decode('ascii').replace(':', ''))).encode('ascii')
2369 except ValueError:
2370 return attr_value
2371
2372
2374 """
2375 Plugin class for fully-qualified DNS domain names
2376 """
2377 oid: str = 'DNSDomain-oid'
2378 desc: str = 'DNS domain name (see RFC 1035)'
2379 pattern = re.compile(r'^(\*|[a-zA-Z0-9_-]+)(\.[a-zA-Z0-9_-]+)*$')
2380 # see https://datatracker.ietf.org/doc/html/rfc2181#section-11
2381 max_len: int = min(255, IA5String.max_len)
2382 sani_funcs = (
2383 bytes.lower,
2384 bytes.strip,
2385 )
2386
2387 def sanitize(self, attr_value: bytes) -> bytes:
2388 attr_value = IA5String.sanitize(self, attr_value)
2389 return b'.'.join([
2390 dc.encode('idna')
2391 for dc in attr_value.decode(self._app.form.accept_charset).split('.')
2392 ])
2393
2394 def form_value(self) -> str:
2395 try:
2396 result = '.'.join([
2397 dc.decode('idna')
2398 for dc in (self._av or b'').split(b'.')
2399 ])
2400 except UnicodeDecodeError:
2401 result = '!!!snipped because of UnicodeDecodeError!!!'
2402 return result
2403
2404 def display(self, vidx, links) -> str:
2405 if self.av_u != self._av.decode('idna'):
2406 return '%s (%s)' % (
2407 IA5String.display(self, vidx, links),
2408 self._app.form.s2d(self.form_valueform_value())
2409 )
2410 return IA5String.display(self, vidx, links)
2411
2412
2414 """
2415 Plugin class for RFC 822 addresses
2416 """
2417 oid: str = 'RFC822Address-oid'
2418 desc: str = 'RFC 822 mail address'
2419 pattern = re.compile(r'^[\w@.+=/_ ()-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*$')
2420 html_tmpl = '<a href="mailto:{av}">{av}</a>'
2421
2422 def __init__(self, app, dn: str, schema, attrType: str, attr_value: bytes, entry=None):
2423 IA5String.__init__(self, app, dn, schema, attrType, attr_value, entry)
2424
2425 def form_value(self) -> str:
2426 if not self._av:
2427 return IA5String.form_value(self)
2428 try:
2429 localpart, domainpart = self._av.rsplit(b'@')
2430 except ValueError:
2431 return IA5String.form_value(self)
2432 dns_domain = DNSDomain(self._app, self._dn, self._schema, None, domainpart)
2433 return '@'.join((
2434 localpart.decode(self._app.ls.charset),
2435 dns_domain.form_value()
2436 ))
2437
2438 def sanitize(self, attr_value: bytes) -> bytes:
2439 try:
2440 localpart, domainpart = attr_value.rsplit(b'@')
2441 except ValueError:
2442 return attr_value
2443 else:
2444 return b'@'.join((
2445 localpart,
2446 DNSDomain.sanitize(self, domainpart)
2447 ))
2448
2449
2451 """
2452 Plugin class for a single DNS label
2453 (see https://datatracker.ietf.org/doc/html/rfc2181#section-11)
2454 """
2455 oid: str = 'DomainComponent-oid'
2456 desc: str = 'DNS domain name component'
2457 pattern = re.compile(r'^(\*|[a-zA-Z0-9_-]+)$')
2458 max_len: int = min(63, DNSDomain.max_len)
2459
2460
2462 """
2463 Plugin class used for JSON data (see RFC 8259)
2464 """
2465 oid: str = 'JSONValue-oid'
2466 desc: str = 'JSON data'
2467 lineSep = b'\n'
2468 mime_type: str = 'application/json'
2469
2470 def _validate(self, attr_value: bytes) -> bool:
2471 try:
2472 json.loads(attr_value)
2473 except ValueError:
2474 return False
2475 return True
2476
2477 def _split_lines(self, value):
2478 try:
2479 obj = json.loads(value)
2480 except ValueError:
2481 return PreformattedMultilineText._split_lines(self, value)
2482 return PreformattedMultilineText._split_lines(
2483 self,
2484 json.dumps(
2485 obj,
2486 indent=4,
2487 separators=(',', ': ')
2488 ).encode('utf-8')
2489 )
2490
2491 def sanitize(self, attr_value: bytes) -> bytes:
2492 try:
2493 obj = json.loads(attr_value)
2494 except ValueError:
2495 return PreformattedMultilineText.sanitize(self, attr_value)
2496 return json.dumps(
2497 obj,
2498 separators=(',', ':')
2499 ).encode('utf-8')
2500
2501
2503 """
2504 Plugin class used for XML data
2505 """
2506 oid: str = 'XmlValue-oid'
2507 desc: str = 'XML data'
2508 lineSep = b'\n'
2509 mime_type: str = 'text/xml'
2510
2511 def _validate(self, attr_value: bytes) -> bool:
2512 if not DEFUSEDXML_AVAIL:
2513 return PreformattedMultilineText._validate(self, attr_value)
2514 try:
2515 defusedxml.ElementTree.XML(attr_value)
2516 except defusedxml.ElementTree.ParseError:
2517 return False
2518 return True
2519
2520
2522 """
2523 Plugin class used for BER-encoded ASN.1 data
2524 """
2525 oid: str = 'ASN1Object-oid'
2526 desc: str = 'BER-encoded ASN.1 data'
2527
2528
2530 """
2531 This base-class class is used for OIDs of cryptographic algorithms
2532 """
2533 oid: str = 'AlgorithmOID-oid'
2534
2535
2537 """
2538 Plugin class for selection of OIDs for hash algorithms
2539 (see https://www.iana.org/assignments/hash-function-text-names/).
2540 """
2541 oid: str = 'HashAlgorithmOID-oid'
2542 desc: str = 'values from https://www.iana.org/assignments/hash-function-text-names/'
2543 attr_value_dict: Dict[str, str] = {
2544 '1.2.840.113549.2.2': 'md2', # [RFC3279]
2545 '1.2.840.113549.2.5': 'md5', # [RFC3279]
2546 '1.3.14.3.2.26': 'sha-1', # [RFC3279]
2547 '2.16.840.1.101.3.4.2.4': 'sha-224', # [RFC4055]
2548 '2.16.840.1.101.3.4.2.1': 'sha-256', # [RFC4055]
2549 '2.16.840.1.101.3.4.2.2': 'sha-384', # [RFC4055]
2550 '2.16.840.1.101.3.4.2.3': 'sha-512', # [RFC4055]
2551 }
2552
2553
2555 """
2556 Plugin class for selection of OIDs for HMAC algorithms (see RFC 8018).
2557 """
2558 oid: str = 'HMACAlgorithmOID-oid'
2559 desc: str = 'values from RFC 8018'
2560 attr_value_dict: Dict[str, str] = {
2561 # from RFC 8018
2562 '1.2.840.113549.2.7': 'hmacWithSHA1',
2563 '1.2.840.113549.2.8': 'hmacWithSHA224',
2564 '1.2.840.113549.2.9': 'hmacWithSHA256',
2565 '1.2.840.113549.2.10': 'hmacWithSHA384',
2566 '1.2.840.113549.2.11': 'hmacWithSHA512',
2567 }
2568
2569
2571 """
2572 This mix-in plugin class composes attribute values from other attribute values.
2573
2574 One can define an ordered sequence of string templates in class
2575 attribute ComposedDirectoryString.compose_templates.
2576 See examples in module web2ldap.app.plugins.inetorgperson.
2577
2578 Obviously this only works for single-valued attributes,
2579 more precisely only the "first" attribute value is used.
2580 """
2581 oid: str = 'ComposedDirectoryString-oid'
2582 compose_templates: Sequence[str] = ()
2583
2584 class SingleValueDict(dict):
2585 """
2586 dictionary-like class which only stores and returns the
2587 first value of an attribute value list
2588 """
2589
2590 def __init__(self, entry, encoding):
2591 dict.__init__(self)
2592 self._encoding = encoding
2593 entry = entry or {}
2594 for key, val in entry.items():
2595 self.__setitem__(key, val)
2596
2597 def __setitem__(self, key, val):
2598 if val and val[0]:
2599 dict.__setitem__(self, key, val[0].decode(self._encoding))
2600
2601 def form_value(self) -> str:
2602 """
2603 Return a dummy value that attribute is returned from input form and
2604 then seen by .transmute()
2605 """
2606 return ''
2607
2608 def transmute(self, attr_values: List[bytes]) -> List[bytes]:
2609 """
2610 always returns a list with a single value based on the first
2611 successfully applied compose template
2612 """
2613 entry = self.SingleValueDict(self._entry, encoding=self._app.ls.charset)
2614 for template in self.compose_templates:
2615 try:
2616 attr_values = [template.format(**entry).encode(self._app.ls.charset)]
2617 except KeyError:
2618 continue
2619 else:
2620 break
2621 else:
2622 return attr_values
2623 return attr_values
2624
2625 def input_field(self) -> web_forms.Field:
2626 """
2627 composed attributes must only have hidden input field
2628 """
2629 input_field = web_forms.HiddenInput(
2630 self._at,
2631 ': '.join([self._at, self.desc]),
2632 self.max_len,
2633 self.max_values,
2634 None,
2635 default=self.form_valueform_value(),
2636 )
2637 input_field.charset = self._app.form.accept_charset
2638 return input_field
2639
2640
2642 """
2643 Plugin base class for attributes with Integer syntax
2644 constrained to valid LDAP result code.
2645 """
2646 oid: str = 'LDAPResultCode-oid'
2647 desc: str = 'LDAPv3 declaration of resultCode in (see RFC 4511)'
2648 attr_value_dict: Dict[str, str] = {
2649 '0': 'success',
2650 '1': 'operationsError',
2651 '2': 'protocolError',
2652 '3': 'timeLimitExceeded',
2653 '4': 'sizeLimitExceeded',
2654 '5': 'compareFalse',
2655 '6': 'compareTrue',
2656 '7': 'authMethodNotSupported',
2657 '8': 'strongerAuthRequired',
2658 '9': 'reserved',
2659 '10': 'referral',
2660 '11': 'adminLimitExceeded',
2661 '12': 'unavailableCriticalExtension',
2662 '13': 'confidentialityRequired',
2663 '14': 'saslBindInProgress',
2664 '16': 'noSuchAttribute',
2665 '17': 'undefinedAttributeType',
2666 '18': 'inappropriateMatching',
2667 '19': 'constraintViolation',
2668 '20': 'attributeOrValueExists',
2669 '21': 'invalidAttributeSyntax',
2670 '32': 'noSuchObject',
2671 '33': 'aliasProblem',
2672 '34': 'invalidDNSyntax',
2673 '35': 'reserved for undefined isLeaf',
2674 '36': 'aliasDereferencingProblem',
2675 '48': 'inappropriateAuthentication',
2676 '49': 'invalidCredentials',
2677 '50': 'insufficientAccessRights',
2678 '51': 'busy',
2679 '52': 'unavailable',
2680 '53': 'unwillingToPerform',
2681 '54': 'loopDetect',
2682 '64': 'namingViolation',
2683 '65': 'objectClassViolation',
2684 '66': 'notAllowedOnNonLeaf',
2685 '67': 'notAllowedOnRDN',
2686 '68': 'entryAlreadyExists',
2687 '69': 'objectClassModsProhibited',
2688 '70': 'reserved for CLDAP',
2689 '71': 'affectsMultipleDSAs',
2690 '80': 'other',
2691 }
2692
2693
2695 oid: str = 'SchemaDescription-oid'
2696 schema_cls = None
2697 sani_funcs = (
2698 bytes.strip,
2699 )
2700
2701 def _validate(self, attr_value: bytes) -> bool:
2702 if self.schema_cls is None:
2703 return DirectoryString._validate(self, attr_value)
2704 try:
2705 _ = self.schema_cls(self._app.ls.uc_decode(attr_value)[0])
2706 except (IndexError, ValueError):
2707 return False
2708 return True
2709
2710
2712 oid: str = '1.3.6.1.4.1.1466.115.121.1.37'
2713 schema_cls = ldap0.schema.models.ObjectClass
2714
2715
2717 oid: str = '1.3.6.1.4.1.1466.115.121.1.3'
2718 schema_cls = ldap0.schema.models.AttributeType
2719
2720
2722 oid: str = '1.3.6.1.4.1.1466.115.121.1.30'
2723 schema_cls = ldap0.schema.models.MatchingRule
2724
2725
2727 oid: str = '1.3.6.1.4.1.1466.115.121.1.31'
2728 schema_cls = ldap0.schema.models.MatchingRuleUse
2729
2730
2732 oid: str = '1.3.6.1.4.1.1466.115.121.1.54'
2733 schema_cls = ldap0.schema.models.LDAPSyntax
2734
2735
2737 oid: str = '1.3.6.1.4.1.1466.115.121.1.16'
2738 schema_cls = ldap0.schema.models.DITContentRule
2739
2740
2742 oid: str = '1.3.6.1.4.1.1466.115.121.1.17'
2743 schema_cls = ldap0.schema.models.DITStructureRule
2744
2745
2747 oid: str = '1.3.6.1.4.1.1466.115.121.1.35'
2748 schema_cls = ldap0.schema.models.NameForm
2749
2750
2751# Set up the central syntax registry instance
2752syntax_registry = SyntaxRegistry()
2753
2754# Register all syntax classes in this module
2755syntax_registry.reg_syntaxes(__name__)
bool _validate(self, bytes attr_value)
Definition: syntaxes.py:494
str display(self, vidx, links)
Definition: syntaxes.py:499
str display(self, vidx, links)
Definition: syntaxes.py:644
web_forms.Field input_field(self)
Definition: syntaxes.py:462
str display(self, vidx, links)
Definition: syntaxes.py:471
def __init__(self, app, str dn, schema, str attrType, bytes attr_value, entry=None)
Definition: syntaxes.py:2271
bytes sanitize(self, bytes attr_value)
Definition: syntaxes.py:2281
bool _validate(self, bytes attr_value)
Definition: syntaxes.py:2219
str display(self, vidx, links)
Definition: syntaxes.py:2224
Dict[str, str] get_attr_value_dict(self)
Definition: syntaxes.py:2211
List[bytes] transmute(self, List[bytes] attr_values)
Definition: syntaxes.py:2608
Dict[str, str] get_attr_value_dict(self)
Definition: syntaxes.py:2239
str display(self, vidx, links)
Definition: syntaxes.py:2404
bytes sanitize(self, bytes attr_value)
Definition: syntaxes.py:2387
bool _validate(self, bytes attr_value)
Definition: syntaxes.py:1695
bytes sanitize(self, bytes attr_value)
Definition: syntaxes.py:1637
bool _validate(self, bytes attr_value)
Definition: syntaxes.py:1627
dict _get_ref_entry(self, str dn, attrlist=None)
Definition: syntaxes.py:2170
bool _validate(self, bytes attr_value)
Definition: syntaxes.py:525
bool _validate(self, bytes attr_value)
Definition: syntaxes.py:549
dict _get_ref_entry(self, str dn, attrlist=None)
Definition: syntaxes.py:2122
bool _validate(self, bytes attr_value)
Definition: syntaxes.py:2141
def __init__(self, app, str dn, schema, str attrType, bytes attr_value, entry=None)
Definition: syntaxes.py:1936
bytes sanitize(self, bytes attr_value)
Definition: syntaxes.py:789
bool _validate(self, bytes attr_value)
Definition: syntaxes.py:768
bool _validate(self, bytes attr_value)
Definition: syntaxes.py:714
bool _validate(self, bytes attr_value)
Definition: syntaxes.py:1007
bool _validate(self, bytes attr_value)
Definition: syntaxes.py:1040
bool _validate(self, bytes attr_value)
Definition: syntaxes.py:1143
str display(self, vidx, links)
Definition: syntaxes.py:1163
bytes sanitize(self, bytes attr_value)
Definition: syntaxes.py:1146
bytes sanitize(self, bytes attr_value)
Definition: syntaxes.py:973
web_forms.Field input_field(self)
Definition: syntaxes.py:979
def __init__(self, app, str dn, schema, str attrType, bytes attr_value, entry=None)
Definition: syntaxes.py:947
bool _validate(self, bytes attr_value)
Definition: syntaxes.py:962
bool _validate(self, bytes attr_value)
Definition: syntaxes.py:2470
bytes sanitize(self, bytes attr_value)
Definition: syntaxes.py:2491
bytes sanitize(self, bytes attr_value)
Definition: syntaxes.py:321
web_forms.Field input_field(self)
Definition: syntaxes.py:432
def __init__(self, app, Optional[str] dn, SubSchema schema, Optional[str] attrType, Optional[bytes] attr_value, entry=None)
Definition: syntaxes.py:292
def validate(self, bytes attr_value)
Definition: syntaxes.py:359
List[bytes] transmute(self, List[bytes] attr_values)
Definition: syntaxes.py:335
str display(self, vidx, links)
Definition: syntaxes.py:446
str value_button(self, command, row, mode, link_text=None)
Definition: syntaxes.py:380
bool _validate(self, bytes attr_value)
Definition: syntaxes.py:351
str display(self, vidx, links)
Definition: syntaxes.py:1316
def _command_ldap_url(self, ldap_url)
Definition: syntaxes.py:1313
bytes sanitize(self, bytes attr_value)
Definition: syntaxes.py:1086
bytes sanitize(self, bytes attr_value)
Definition: syntaxes.py:1413
bool _validate(self, bytes attr_value)
Definition: syntaxes.py:678
Tuple[str, Optional[str]] _split_dn_and_uid(str val)
Definition: syntaxes.py:667
str display(self, vidx, links)
Definition: syntaxes.py:1260
str value_button(self, command, row, mode, link_text=None)
Definition: syntaxes.py:1252
bytes sanitize(self, bytes attr_value)
Definition: syntaxes.py:1257
str display(self, vidx, links)
Definition: syntaxes.py:1598
bytes sanitize(self, bytes attr_value)
Definition: syntaxes.py:1347
bytes sanitize(self, bytes attr_value)
Definition: syntaxes.py:1482
def __init__(self, app, str dn, schema, str attrType, bytes attr_value, entry=None)
Definition: syntaxes.py:2422
bytes sanitize(self, bytes attr_value)
Definition: syntaxes.py:2438
bool _validate(self, bytes attr_value)
Definition: syntaxes.py:2701
bool _validate(self, bytes attr_value)
Definition: syntaxes.py:1850
str display(self, vidx, links)
Definition: syntaxes.py:1854
web_forms.Field input_field(self)
Definition: syntaxes.py:1874
Dict[str, str] get_attr_value_dict(self)
Definition: syntaxes.py:1816
def reg_at(self, str syntax_oid, attr_types, structural_oc_oids=None)
Definition: syntaxes.py:143
def get_at(self, app, dn, schema, attr_type, attr_value, entry=None)
Definition: syntaxes.py:210
def get_syntax(self, schema, attrtype_nameoroid, structural_oc)
Definition: syntaxes.py:175
bytes sanitize(self, bytes attr_value)
Definition: syntaxes.py:1534
web_forms.Field input_field(self)
Definition: syntaxes.py:1788
bytes sanitize(self, bytes attr_value)
Definition: syntaxes.py:1766
str display(self, vidx, links)
Definition: syntaxes.py:1791
bytes sanitize(self, bytes attr_value)
Definition: syntaxes.py:2366
str display(self, vidx, links)
Definition: syntaxes.py:1104
bool _validate(self, bytes attr_value)
Definition: syntaxes.py:2511
def schema_anchor(app, se_nameoroid, se_class, name_template='{name}\n{anchor}', link_text=None)
Definition: __init__.py:205
def get_variant_filename(pathname, variantlist)
Definition: tmpl.py:20
def chunks(l, s)
Definition: msbase.py:67
str ascii_dump(bytes buf, str repl='.')
Definition: msbase.py:79
def strftimeiso8601(t)
Definition: utctime.py:65
str ts2repr(Sequence[Tuple[str, int]] time_divisors, str ts_sep, Union[str, bytes] ts_value)
Definition: utctime.py:79
def repr2ts(time_divisors, ts_sep, value)
Definition: utctime.py:92
def cmp(val1, val2)
Definition: __init__.py:57