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