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