ignore illegal characters in xml content handler (should fix broken xml in some brows...
[enigma2-plugins.git] / webinterface / src / webif.py
1 # -*- coding: UTF-8 -*-
2 Version = '$Header$';
3
4 # things to improve:
5 #       - better error handling
6 #       - use namespace parser
7
8 from Tools.Import import my_import
9
10 from Components.Sources.Source import ObsoleteSource
11 from Components.Converter.Converter import Converter
12 from Components.Element import Element
13
14 from xml.sax import make_parser
15 from xml.sax.handler import ContentHandler, feature_namespaces
16 from xml.sax.saxutils import escape as escape_xml
17 from twisted.python import util
18 from urllib2 import quote
19
20 #DO NOT REMOVE THIS IMPORT
21 #It IS used (dynamically)
22 from WebScreens import *
23 #DO NOT REMOVE THIS IMPORT
24                 
25
26 # The classes and Function in File handle all ScreenPage-based requests
27 # ScreenPages use enigma2 standard functionality to bring contents to a webfrontend
28 #
29 # Like Skins a ScreenPage can consist of several Elements and Converters
30
31 #===============================================================================
32 # OneTimeElement
33 #
34 # This is the Standard Element for Rendering a "standard" WebElement
35 #===============================================================================
36 class OneTimeElement(Element):
37         def __init__(self, id):
38                 Element.__init__(self)
39                 self.source_id = id
40
41         def handleCommand(self, args):
42                 if ',' in self.source_id:
43                         paramlist = self.source_id.split(",")
44                         list = {}
45                         for key in paramlist:
46                                 arg = args.get(key, ())
47                                 Len = len(arg)
48                                 if Len == 0:
49                                         list[key] = None
50                                 elif Len == 1:
51                                         list[key] = "".join(arg)
52                                 elif Len == 2:
53                                         list[key] = arg[0]
54                         self.source.handleCommand(list)
55                 else:
56                         for c in args.get(self.source_id, ()):
57                                 self.source.handleCommand(c)
58
59         def render(self, request):
60                 t = self.source.getHTML(self.source_id)
61                 request.write(t)
62
63         def execBegin(self):
64                 self.suspended = False
65
66         def execEnd(self):
67                 self.suspended = True
68
69         def onShow(self):
70                 pass
71
72         def onHide(self):
73                 pass
74
75         def destroy(self):
76                 pass
77
78 #===============================================================================
79 # MacroElement
80 #
81 # A MacroElement helps using OneTimeElements inside a (Simple)ListFiller Loop
82 #===============================================================================
83 class MacroElement(OneTimeElement):
84         def __init__(self, id, macro_dict, macro_name):
85                 OneTimeElement.__init__(self, id)
86                 self.macro_dict = macro_dict
87                 self.macro_name = macro_name
88
89         def render(self, request):
90                 self.macro_dict[self.macro_name] = self.source.getHTML(self.source_id)
91
92 #===============================================================================
93 # StreamingElement
94 #
95 # In difference to an OneTimeElement a StreamingElement sends an ongoing Stream
96 # of Data. The end of the Streaming is usually when the client disconnects
97 #===============================================================================
98 class StreamingElement(OneTimeElement):
99         def __init__(self, id):
100                 OneTimeElement.__init__(self, id)
101                 self.request = None
102
103         def changed(self, what):
104                 if self.request:
105                         self.render(self.request)
106
107         def setRequest(self, request):
108                 self.request = request
109
110 #===============================================================================
111 # ListItem
112 #
113 # a to-be-filled list item
114 #===============================================================================
115 class ListItem:
116         def __init__(self, name, filternum):
117                 self.name = name
118                 self.filternum = filternum
119
120 #===============================================================================
121 # ListMacroItem
122 #
123 # MacroItem inside a (Simple)ListFiller
124 #===============================================================================
125 class ListMacroItem:
126         def __init__(self, macrodict, macroname):
127                 self.macrodict = macrodict
128                 self.macroname = macroname
129
130
131 #===============================================================================
132 # TextToHTML
133 #
134 # Returns the String as is
135 #===============================================================================
136 class TextToHTML(Converter):
137         def __init__(self, arg):
138                 Converter.__init__(self, arg)
139
140         def getHTML(self, id):
141                 return self.source.text.replace('\xc2\x86', '').replace('\xc2\x87', '') # encode & etc. here!
142
143 #===============================================================================
144 # TextToXML
145 #
146 # Escapes the given Text to be XML conform
147 #===============================================================================
148 class TextToXML(Converter):
149         def __init__(self, arg):
150                 Converter.__init__(self, arg)
151
152         def getHTML(self, id):
153                 return escape_xml(self.source.text).replace("\x19", "").replace("\x1c", "").replace("\x1e", "").replace('\xc2\x86', '').replace('\xc2\x87', '')
154
155 #===============================================================================
156 # TextToURL
157 #
158 # Escapes the given Text so it can be used inside a URL
159 #===============================================================================
160 class TextToURL(Converter):
161         def __init__(self, arg):
162                 Converter.__init__(self, arg)
163
164         def getHTML(self, id):
165                 return self.source.text.replace(" ", "%20").replace("+", "%2b").replace("&", "%26").replace('\xc2\x86', '').replace('\xc2\x87', '')
166
167 #===============================================================================
168 # ReturnEmptyXML
169
170 # Returns a XML only consisting of <rootElement />
171 #===============================================================================
172 class ReturnEmptyXML(Converter):
173         def __init__(self, arg):
174                 Converter.__init__(self, arg)
175
176         def getHTML(self, id):
177                 return "<rootElement />"
178
179 #===============================================================================
180 # Null
181 # Return simply NOTHING
182 # Useful if you only want to issue a command.
183 #===============================================================================
184 class Null(Converter):
185         def __init__(self, arg):
186                 Converter.__init__(self, arg)
187
188         def getHTML(self, id):
189                 return ""
190
191
192 #===============================================================================
193 # JavascriptUpdate
194 #
195 # Transforms a string into a javascript update pattern
196 #===============================================================================
197 class JavascriptUpdate(Converter):
198         def __init__(self, arg):
199                 Converter.__init__(self, arg)
200
201         def getHTML(self, id):
202                 # 3c5x9, added parent. , this is because the ie loads this in a iframe. an the set is in index.html.xml
203                 #                all other will replace this in JS
204                 return '<script>parent.set("%s", "%s");</script>\n' % (id, self.source.text.replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"').replace('\xb0', '&deg;'))
205
206 #===============================================================================
207 # SimpleListFiller
208 #
209 # The performant 'one-dimensonial listfiller' engine (podlfe)
210 #===============================================================================
211 class SimpleListFiller(Converter):
212         def __init__(self, arg):
213                 Converter.__init__(self, arg)
214                 
215         def getText(self):
216                 l = self.source.simplelist
217                 conv_args = self.converter_arguments            
218                 
219                 list = [ ]
220                 for element in conv_args:
221                         if isinstance(element, basestring):
222                                 list.append((element, None))
223                         elif isinstance(element, ListItem):
224                                 list.append((element, element.filternum))
225                         elif isinstance(element, ListMacroItem):
226                                 list.append(element.macrodict[element.macroname], None)
227                         else:
228                                 raise Exception("neither string, ListItem nor ListMacroItem")
229                         
230                 strlist = [ ]
231                 append = strlist.append
232                 for item in l:
233                         if item is None:
234                                 item = ""
235                                 
236                         for (element, filternum) in list:
237                                 item = str(item).replace('\xc2\x86', '').replace('\xc2\x87', '')
238                                 
239                                 if not filternum:
240                                         append(element)
241                                 elif filternum == 2:
242                                         append(item.replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"'))
243                                 elif filternum == 3:                                    
244                                         append(escape_xml( item.replace("\x19", "").replace("\x1c", "").replace("\x1e", "").replace('\xc2\x86', '').replace('\xc2\x87', '') ))
245                                 elif filternum == 4:
246                                         append(item.replace("%", "%25").replace("+", "%2B").replace('&', '%26').replace('?', '%3f').replace(' ', '+'))
247                                 elif filternum == 5:
248                                         append(quote(item))
249                                 elif filternum == 6:
250                                         time = parseint(item) or 0
251                                         t = localtime(time)
252                                         append("%02d:%02d" % (t.tm_hour, t.tm_min))
253                                 elif filternum == 7:
254                                         time = parseint(item) or 0
255                                         t = localtime(time)
256                                         append("%d min" % (time / 60))
257                                 else:
258                                         append(item)
259                 # (this will be done in c++ later!)
260
261                 return ''.join(strlist)         
262         
263         text = property(getText)
264                         
265 #===============================================================================
266 # the performant 'listfiller'-engine (plfe)
267 #===============================================================================
268 class ListFiller(Converter):
269         def __init__(self, arg):
270                 Converter.__init__(self, arg)
271 #               print "ListFiller-arg: ",arg
272
273         def getText(self):
274                 l = self.source.list
275                 lut = self.source.lut
276                 conv_args = self.converter_arguments
277
278                 # now build a ["string", 1, "string", 2]-styled list, with indices into the
279                 # list to avoid lookup of item name for each entry
280                 lutlist = [ ]
281                 for element in conv_args:
282                         if isinstance(element, basestring):
283                                 lutlist.append((element, None))
284                         elif isinstance(element, ListItem):
285                                 lutlist.append((lut[element.name], element.filternum))
286                         elif isinstance(element, ListMacroItem):
287                                 lutlist.append((element.macrodict[element.macroname], None))
288                         else:
289                                 raise Exception("neither string, ListItem nor ListMacroItem")
290
291                 # now, for the huge list, do:
292                 strlist = [ ]
293                 append = strlist.append
294                 for item in l:
295                         for (element, filternum) in lutlist:                    
296                                 #None becomes ""
297                                 curitem = ""
298                                 if filternum:
299                                         curitem = str(item[element]).replace('\xc2\x86', '').replace('\xc2\x87', '')
300                                         if curitem is None:
301                                                 curitem = ""
302                                 else:
303                                         if element is None:
304                                                 element = ""
305                                                 
306                                 if not filternum:
307                                         append(element)
308                                 elif filternum == 2:
309                                         append(curitem.replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"'))
310                                 elif filternum == 3:
311                                         append( escape_xml( curitem.replace("\x19", "").replace("\x1c", "").replace("\x1e", "").replace('\xc2\x86', '').replace('\xc2\x87', '') ))
312                                 elif filternum == 4:
313                                         append(curitem.replace("%", "%25").replace("+", "%2B").replace('&', '%26').replace('?', '%3f').replace(' ', '+'))
314                                 elif filternum == 5:
315                                         append(quote(curitem))
316                                 elif filternum == 6:
317                                         from time import localtime
318                                         time = int(float(curitem)) or 0
319                                         t = localtime(time)
320                                         append("%02d:%02d" % (t.tm_hour, t.tm_min))
321                                 elif filternum == 7:
322                                         from time import localtime
323                                         time = int(float(curitem)) or 0
324                                         t = localtime(time)
325                                         append("%d min" % (time / 60))                                  
326                                 else:
327                                         append(curitem)
328                 # (this will be done in c++ later!)
329
330                 return ''.join(strlist)
331
332         text = property(getText)
333
334 #===============================================================================
335 # webifHandler
336 #
337 # Handles the Content of a Web-Request
338 # It looks up the source, instantiates the Element and Calls the Converter
339 #===============================================================================
340 class webifHandler(ContentHandler):
341         def __init__(self, session, request):
342                 self.res = [ ]
343                 self.mode = 0
344                 self.screen = None
345                 self.session = session
346                 self.screens = [ ]
347                 self.request = request
348                 self.macros = { }
349
350         def start_element(self, attrs):
351                 scr = self.screen
352
353                 wsource = attrs["source"]
354
355                 path = wsource.split('.')
356                 while len(path) > 1:
357                         scr = self.screen.getRelatedScreen(path[0])
358                         if scr is None:
359                                 print "[webif.py] Parent Screen not found!"
360                                 print wsource
361                         path = path[1:]
362
363                 source = scr.get(path[0])
364
365                 if isinstance(source, ObsoleteSource):
366                         # however, if we found an "obsolete source", issue warning, and resolve the real source.
367                         print "WARNING: WEBIF '%s' USES OBSOLETE SOURCE '%s', USE '%s' INSTEAD!" % (name, wsource, source.new_source)
368                         print "OBSOLETE SOURCE WILL BE REMOVED %s, PLEASE UPDATE!" % (source.removal_date)
369                         if source.description:
370                                 print source.description
371
372                         wsource = source.new_source
373                 else:
374                         pass
375                         # otherwise, use that source.
376
377                 self.source = source
378                 self.source_id = str(attrs.get("id", wsource))
379                 self.is_streaming = "streaming" in attrs
380                 self.macro_name = attrs.get("macro") or None
381
382         def end_element(self):
383                 # instatiate either a StreamingElement or a OneTimeElement, depending on what's required.
384                 if not self.is_streaming:
385                         if self.macro_name is None:
386                                 c = OneTimeElement(self.source_id)
387                         else:
388                                 c = MacroElement(self.source_id, self.macros, self.macro_name)
389                 else:
390                         assert self.macro_name is None
391                         c = StreamingElement(self.source_id)
392
393                 c.connect(self.source)
394                 self.res.append(c)
395                 self.screen.renderer.append(c)
396                 del self.source
397
398         def start_convert(self, attrs):
399                 ctype = attrs["type"]
400
401                 # TODO: we need something better here
402                 if ctype[:4] == "web:": # for now
403                         self.converter = eval(ctype[4:])
404                 else:
405                         try:
406                                 self.converter = my_import('.'.join(("Components", "Converter", ctype))).__dict__.get(ctype)
407                         except ImportError:
408                                 self.converter = my_import('.'.join(("Plugins", "Extensions", "WebInterface", "WebComponents", "Converter", ctype))).__dict__.get(ctype)
409                 self.sub = [ ]
410
411         def end_convert(self):
412                 if len(self.sub) == 1:
413                         self.sub = self.sub[0]
414                 c = self.converter(self.sub)
415                 c.connect(self.source)
416                 self.source = c
417                 del self.sub
418
419         def parse_item(self, attrs):
420                 if "name" in attrs:
421                         filter = {"": 1, "javascript_escape": 2, "xml": 3, "uri": 4, "urlencode": 5, "time": 6, "minutes": 7}[attrs.get("filter", "")]
422                         self.sub.append(ListItem(attrs["name"], filter))
423                 else:
424                         assert "macro" in attrs, "e2:item must have a name= or macro= attribute!"
425                         self.sub.append(ListMacroItem(self.macros, attrs["macro"]))
426
427         def startElement(self, name, attrs):
428                 if name == "e2:screen":
429                         self.screen = eval(attrs["name"])(self.session, self.request) # fixme
430                         self.screens.append(self.screen)
431                         return
432
433                 if name[:3] == "e2:":
434                         self.mode += 1
435
436                 tag = '<' + name + ''.join([' %s="%s"' % x for x in attrs.items()]) + '>'
437                 #tag = tag.encode('utf-8')
438
439                 if self.mode == 0:
440                         self.res.append(tag)
441                 elif self.mode == 1: # expect "<e2:element>"
442                         assert name == "e2:element", "found %s instead of e2:element" % name
443                         self.start_element(attrs)
444                 elif self.mode == 2: # expect "<e2:convert>"
445                         if name[:3] == "e2:":
446                                 assert name == "e2:convert"
447                                 self.start_convert(attrs)
448                         else:
449                                 self.sub.append(tag)
450                 elif self.mode == 3:
451                         assert name == "e2:item", "found %s instead of e2:item!" % name
452
453                         self.parse_item(attrs)
454
455         def endElement(self, name):
456                 if name == "e2:screen":
457                         self.screen = None
458                         return
459
460                 tag = "</" + name + ">"
461                 if self.mode == 0:
462                         self.res.append(tag)
463                 elif self.mode == 2 and name[:3] != "e2:":
464                         self.sub.append(tag)
465                 elif self.mode == 2: # closed 'convert' -> sub
466                         self.end_convert()
467                 elif self.mode == 1: # closed 'element'
468                         self.end_element()
469                 if name[:3] == "e2:":
470                         self.mode -= 1
471
472         def processingInstruction(self, target, data):
473                 self.res.append('<?' + target + ' ' + data + '>')
474
475         def characters(self, ch):
476                 ch = ch.encode('utf-8', 'ignore')
477                 if self.mode == 0:
478                         self.res.append(ch)
479                 elif self.mode == 2:
480                         self.sub.append(ch)
481
482         def startEntity(self, name):
483                 self.res.append('&' + name + ';');
484
485         def execBegin(self):
486                 for screen in self.screens:
487                         screen.execBegin()
488
489         def cleanup(self):
490                 print "screen cleanup!"
491                 for screen in self.screens:
492                         screen.execEnd()
493                         screen.doClose()
494                 self.screens = [ ]
495
496 #===============================================================================
497 # renderPage
498 #
499 # Creates the Handler for a Request and calls it
500 # Also ensures that the Handler is finished after the Request is done
501 #===============================================================================
502 def renderPage(request, path, session):
503         # read in the template, create required screens
504         # we don't have persistense yet.
505         # if we had, this first part would only be done once.
506         handler = webifHandler(session, request)
507         parser = make_parser()
508         parser.setFeature(feature_namespaces, 0)
509         parser.setContentHandler(handler)
510         parser.parse(open(util.sibpath(__file__, path)))
511
512         # by default, we have non-streaming pages
513         finish = True
514
515         # first, apply "commands" (aka. URL argument)
516         for x in handler.res:
517                 if isinstance(x, Element):
518                         x.handleCommand(request.args)
519
520         handler.execBegin()
521
522         # now, we have a list with static texts mixed
523         # with non-static Elements.
524         # flatten this list, write into the request.
525         for x in handler.res:
526                 if isinstance(x, Element):
527                         if isinstance(x, StreamingElement):
528                                 finish = False
529                                 x.setRequest(request)
530                         x.render(request)
531                 else:
532                         request.write(str(x))
533
534         # if we met a "StreamingElement", there is at least one
535         # element which wants to output data more than once,
536         # i.e. on host-originated changes.
537         # in this case, don't finish yet, don't cleanup yet,
538         # but instead do that when the client disconnects.
539         if finish:
540                 requestFinish(handler, request)
541         
542         else:   
543                 def requestFinishDeferred(nothing, handler, request):
544                         from twisted.internet import reactor
545                         reactor.callLater(0, requestFinish, handler, request)                           
546                 
547                 d = request.notifyFinish()
548
549                 d.addBoth( requestFinishDeferred, handler, request )
550                                                         
551 #===============================================================================
552 # requestFinish
553 #
554 # This has to be/is called at the end of every ScreenPage-based Request
555 #===============================================================================
556 def requestFinish(handler, request):
557         handler.cleanup()
558         request.finish()        
559         
560         del handler