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