[MerlinSkinThemes] - add config option to disable rebuild on boot
[enigma2-plugins.git] / fritzcall / src / ldif.py
1 #@PydevCodeAnalysisIgnore
2 # pylint: disable-msg=W0312,C0324,C0322
3
4 """
5 ldif - generate and parse LDIF data (see RFC 2849)
6
7 See http://python-ldap.sourceforge.net for details.
8
9 $Id: ldif.py 1401 2017-05-06 17:37:48Z michael $
10
11 Python compability note:
12 Tested with Python 2.0+, but should work with Python 1.5.2+.
13 """
14
15 __version__ = '0.5.5'
16
17 __all__ = [
18   # constants
19   'ldif_pattern',
20   # functions
21   # 'AttrTypeandValueLDIF',
22   'CreateLDIF','ParseLDIF',
23   # classes
24   'LDIFWriter',
25   'LDIFParser',
26   'LDIFRecordList',
27   'LDIFCopy',
28 ]
29
30 import urlparse,urllib,base64,re,types
31
32 try:
33   from cStringIO import StringIO
34 except ImportError:
35   from StringIO import StringIO
36
37 attrtype_pattern = r'[\w;.]+(;[\w_-]+)*'
38 attrvalue_pattern = r'(([^,]|\\,)+|".*?")'
39 rdn_pattern = attrtype_pattern + r'[ ]*=[ ]*' + attrvalue_pattern
40 dn_pattern   = rdn_pattern + r'([ ]*,[ ]*' + rdn_pattern + r')*[ ]*'
41 dn_regex   = re.compile('^%s$' % dn_pattern)
42
43 ldif_pattern = '^((dn(:|::) %(dn_pattern)s)|(%(attrtype_pattern)s(:|::) .*)$)+' % vars()
44
45 MOD_OP_INTEGER = {
46   'add':0,'delete':1,'replace':2
47 }
48
49 MOD_OP_STR = {
50   0:'add',1:'delete',2:'replace'
51 }
52
53 CHANGE_TYPES = ['add','delete','modify','modrdn']
54 valid_changetype_dict = {}
55 for c in CHANGE_TYPES:
56   valid_changetype_dict[c]=None
57
58
59 SAFE_STRING_PATTERN = '(^(\000|\n|\r| |:|<)|[\000\n\r\200-\377]+|[ ]+$)'
60 safe_string_re = re.compile(SAFE_STRING_PATTERN)
61
62 def is_dn(s):
63   """
64   returns 1 if s is a LDAP DN
65   """
66   if s=='':
67     return 1
68   rm = dn_regex.match(s)
69   return rm!=None and rm.group(0)==s
70
71
72 def needs_base64(s):
73   """
74   returns 1 if s has to be base-64 encoded because of special chars
75   """
76   return not safe_string_re.search(s) is None
77
78
79 def list_dict(l):
80   """
81   return a dictionary with all items of l being the keys of the dictionary
82   """
83   return dict([(i,None) for i in l])
84
85
86 class LDIFWriter:
87   """
88   Write LDIF entry or change records to file object
89   Copy LDIF input to a file output object containing all data retrieved
90   via URLs
91   """
92
93   def __init__(self,output_file,base64_attrs=None,cols=76,line_sep='\n'):
94     """
95     output_file
96         file object for output
97     base64_attrs
98         list of attribute types to be base64-encoded in any case
99     cols
100         Specifies how many columns a line may have before it's
101         folded into many lines.
102     line_sep
103         String used as line separator
104     """
105     self._output_file = output_file
106     self._base64_attrs = list_dict([a.lower() for a in (base64_attrs or [])])
107     self._cols = cols
108     self._line_sep = line_sep
109     self.records_written = 0
110
111   def _unfoldLDIFLine(self,line):
112     """
113     Write string line as one or more folded lines
114     """
115     # Check maximum line length
116     line_len = len(line)
117     if line_len<=self._cols:
118       self._output_file.write(line)
119       self._output_file.write(self._line_sep)
120     else:
121       # Fold line
122       pos = self._cols
123       self._output_file.write(line[0:min(line_len,self._cols)])
124       self._output_file.write(self._line_sep)
125       while pos<line_len:
126         self._output_file.write(' ')
127         self._output_file.write(line[pos:min(line_len,pos+self._cols-1)])
128         self._output_file.write(self._line_sep)
129         pos = pos+self._cols-1
130     return # _unfoldLDIFLine()
131
132   def _unparseAttrTypeandValue(self,attr_type,attr_value):
133     """
134     Write a single attribute type/value pair
135
136     attr_type
137           attribute type
138     attr_value
139           attribute value
140     """
141     if self._base64_attrs.has_key(attr_type.lower()) or \
142        needs_base64(attr_value):
143       # Encode with base64
144       self._unfoldLDIFLine(':: '.join([attr_type,base64.encodestring(attr_value).replace('\n','')]))
145     else:
146       self._unfoldLDIFLine(': '.join([attr_type,attr_value]))
147     return # _unparseAttrTypeandValue()
148
149   def _unparseEntryRecord(self,entry):
150     """
151     entry
152         dictionary holding an entry
153     """
154     attr_types = entry.keys()[:]
155     attr_types.sort()
156     for attr_type in attr_types:
157       for attr_value in entry[attr_type]:
158         self._unparseAttrTypeandValue(attr_type,attr_value)
159
160   def _unparseChangeRecord(self,modlist):
161     """
162     modlist
163         list of additions (2-tuple) or modifications (3-tuple)
164     """
165     mod_len = len(modlist[0])
166     if mod_len==2:
167       changetype = 'add'
168     elif mod_len==3:
169       changetype = 'modify'
170     else:
171       raise ValueError,"modlist item of wrong length"
172     self._unparseAttrTypeandValue('changetype',changetype)
173     for mod in modlist:
174       if mod_len==2:
175         mod_type,mod_vals = mod
176       elif mod_len==3:
177         mod_op,mod_type,mod_vals = mod
178         self._unparseAttrTypeandValue(MOD_OP_STR[mod_op],mod_type)
179       else:
180         raise ValueError,"Subsequent modlist item of wrong length"
181       if mod_vals:
182         for mod_val in mod_vals:
183           self._unparseAttrTypeandValue(mod_type,mod_val)
184       if mod_len==3:
185         self._output_file.write('-'+self._line_sep)
186
187   def unparse(self,dn,record):
188     """
189     dn
190           string-representation of distinguished name
191     record
192           Either a dictionary holding the LDAP entry {attrtype:record}
193           or a list with a modify list like for LDAPObject.modify().
194     """
195     if not record:
196       # Simply ignore empty records
197       return
198     # Start with line containing the distinguished name
199     self._unparseAttrTypeandValue('dn',dn)
200     # Dispatch to record type specific writers
201     if isinstance(record,types.DictType):
202       self._unparseEntryRecord(record)
203     elif isinstance(record,types.ListType):
204       self._unparseChangeRecord(record)
205     else:
206       raise ValueError, "Argument record must be dictionary or list"
207     # Write empty line separating the records
208     self._output_file.write(self._line_sep)
209     # Count records written
210     self.records_written = self.records_written+1
211     return # unparse()
212
213
214 def CreateLDIF(dn,record,base64_attrs=None,cols=76):
215   """
216   Create LDIF single formatted record including trailing empty line.
217   This is a compability function. Use is deprecated!
218
219   dn
220         string-representation of distinguished name
221   record
222         Either a dictionary holding the LDAP entry {attrtype:record}
223         or a list with a modify list like for LDAPObject.modify().
224   base64_attrs
225         list of attribute types to be base64-encoded in any case
226   cols
227         Specifies how many columns a line may have before it's
228         folded into many lines.
229   """
230   f = StringIO()
231   ldif_writer = LDIFWriter(f,base64_attrs,cols,'\n')
232   ldif_writer.unparse(dn,record)
233   s = f.getvalue()
234   f.close()
235   return s
236
237
238 class LDIFParser:
239   """
240   Base class for a LDIF parser. Applications should sub-class this
241   class and override method handle() to implement something meaningful.
242
243   Public class attributes:
244   records_read
245         Counter for records processed so far
246   """
247
248   def _stripLineSep(self,s):
249     """
250     Strip trailing line separators from s, but no other whitespaces
251     """
252     if s[-2:]=='\r\n':
253       return s[:-2]
254     elif s[-1:]=='\n':
255       return s[:-1]
256     else:
257       return s
258
259   def __init__(
260     self,
261     input_file,
262     ignored_attr_types=None,
263     max_entries=0,
264     process_url_schemes=None,
265     line_sep='\n'
266   ):
267     """
268     Parameters:
269     input_file
270         File-object to read the LDIF input from
271     ignored_attr_types
272         Attributes with these attribute type names will be ignored.
273     max_entries
274         If non-zero specifies the maximum number of entries to be
275         read from f.
276     process_url_schemes
277         List containing strings with URLs schemes to process with urllib.
278         An empty list turns off all URL processing and the attribute
279         is ignored completely.
280     line_sep
281         String used as line separator
282     """
283     self._input_file = input_file
284     self._max_entries = max_entries
285     self._process_url_schemes = list_dict([s.lower() for s in (process_url_schemes or [])])
286     self._ignored_attr_types = list_dict([a.lower() for a in (ignored_attr_types or [])])
287     self._line_sep = line_sep
288     self.records_read = 0
289
290   def handle(self,dn,entry):
291     """
292     Process a single content LDIF record. This method should be
293     implemented by applications using LDIFParser.
294     """
295
296   def _unfoldLDIFLine(self):
297     """
298     Unfold several folded lines with trailing space into one line
299     """
300     unfolded_lines = [ self._stripLineSep(self._line) ]
301     self._line = self._input_file.readline()
302     while self._line and self._line[0]==' ':
303       unfolded_lines.append(self._stripLineSep(self._line[1:]))
304       self._line = self._input_file.readline()
305     return ''.join(unfolded_lines)
306
307   def _parseAttrTypeandValue(self):
308     """
309     Parse a single attribute type and value pair from one or
310     more lines of LDIF data
311     """
312     # Reading new attribute line
313     unfolded_line = self._unfoldLDIFLine()
314     # Ignore comments which can also be folded
315     while unfolded_line and unfolded_line[0]=='#':
316       unfolded_line = self._unfoldLDIFLine()
317     if not unfolded_line or unfolded_line=='\n' or unfolded_line=='\r\n':
318       return None,None
319     try:
320       colon_pos = unfolded_line.index(':')
321     except ValueError:
322       # Treat malformed lines without colon as non-existent
323       return None,None
324     attr_type = unfolded_line[0:colon_pos]
325     # if needed attribute value is BASE64 decoded
326     value_spec = unfolded_line[colon_pos:colon_pos+2]
327     if value_spec=='::':
328       # attribute value needs base64-decoding
329       attr_value = base64.decodestring(unfolded_line[colon_pos+2:])
330     elif value_spec==':<':
331       # fetch attribute value from URL
332       url = unfolded_line[colon_pos+2:].strip()
333       attr_value = None
334       if self._process_url_schemes:
335         u = urlparse.urlparse(url)
336         if self._process_url_schemes.has_key(u[0]):
337           attr_value = urllib.urlopen(url).read()
338     elif value_spec==':\r\n' or value_spec=='\n':
339       attr_value = ''
340     else:
341       attr_value = unfolded_line[colon_pos+2:].lstrip()
342     return attr_type,attr_value
343
344   def parse(self):
345     """
346     Continously read and parse LDIF records
347     """
348     self._line = self._input_file.readline()
349
350     while self._line and \
351           (not self._max_entries or self.records_read<self._max_entries):
352
353       # Reset record
354       version = None; dn = None; changetype = None; modop = None; entry = {}
355
356       attr_type,attr_value = self._parseAttrTypeandValue()
357
358       while attr_type!=None and attr_value!=None:
359         if attr_type=='dn':
360           # attr type and value pair was DN of LDIF record
361           if dn!=None:
362             raise ValueError, 'Two lines starting with dn: in one record.'
363           if not is_dn(attr_value):
364             raise ValueError, 'No valid string-representation of distinguished name %s.' % (repr(attr_value))
365           dn = attr_value
366         elif attr_type=='version' and dn is None:
367           version = 1
368         elif attr_type=='changetype':
369           # attr type and value pair was DN of LDIF record
370           if dn is None:
371             raise ValueError, 'Read changetype: before getting valid dn: line.'
372           if changetype!=None:
373             raise ValueError, 'Two lines starting with changetype: in one record.'
374           if not valid_changetype_dict.has_key(attr_value):
375             raise ValueError, 'changetype value %s is invalid.' % (repr(attr_value))
376           changetype = attr_value
377         elif attr_value!=None and \
378              not self._ignored_attr_types.has_key(attr_type.lower()):
379           # Add the attribute to the entry if not ignored attribute
380           if entry.has_key(attr_type):
381             entry[attr_type].append(attr_value)
382           else:
383             entry[attr_type]=[attr_value]
384
385         # Read the next line within an entry
386         attr_type,attr_value = self._parseAttrTypeandValue()
387
388       if entry:
389         # append entry to result list
390         self.handle(dn,entry)
391         self.records_read = self.records_read+1
392
393     return # parse()
394
395
396 class LDIFRecordList(LDIFParser):
397   """
398   Collect all records of LDIF input into a single list.
399   of 2-tuples (dn,entry). It can be a memory hog!
400   """
401
402   def __init__(
403     self,
404     input_file,
405     ignored_attr_types=None,max_entries=0,process_url_schemes=None
406   ):
407     """
408     See LDIFParser.__init__()
409
410     Additional Parameters:
411     all_records
412         List instance for storing parsed records
413     """
414     LDIFParser.__init__(self,input_file,ignored_attr_types,max_entries,process_url_schemes)
415     self.all_records = []
416
417   def handle(self,dn,entry):
418     """
419     Append single record to dictionary of all records.
420     """
421     self.all_records.append((dn,entry))
422
423
424 class LDIFCopy(LDIFParser):
425   """
426   Copy LDIF input to LDIF output containing all data retrieved
427   via URLs
428   """
429
430   def __init__(
431     self,
432     input_file,output_file,
433     ignored_attr_types=None,max_entries=0,process_url_schemes=None,
434     base64_attrs=None,cols=76,line_sep='\n'
435   ):
436     """
437     See LDIFParser.__init__() and LDIFWriter.__init__()
438     """
439     LDIFParser.__init__(self,input_file,ignored_attr_types,max_entries,process_url_schemes)
440     self._output_ldif = LDIFWriter(output_file,base64_attrs,cols,line_sep)
441
442   def handle(self,dn,entry):
443     """
444     Write single LDIF record to output file.
445     """
446     self._output_ldif.unparse(dn,entry)
447
448
449 def ParseLDIF(f,ignore_attrs=None,maxentries=0):
450   """
451   Parse LDIF records read from file.
452   This is a compability function. Use is deprecated!
453   """
454   ldif_parser = LDIFRecordList(
455     f,ignored_attr_types=ignore_attrs,max_entries=maxentries,process_url_schemes=0
456   )
457   ldif_parser.parse()
458   return ldif_parser.all_records