Webinterface: check fullpath in getMovieSubdirs
[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 == webifHandler.FILTER_JAVASCRIPT:
277                 append(item.replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"'))
278         elif filternum == webifHandler.FILTER_XML:
279                 append( escape_xml( item ))
280         elif filternum == webifHandler.FILTER_URI:
281                 append(item.replace("%", "%25").replace("+", "%2B").replace('&', '%26').replace('?', '%3f').replace(' ', '+'))
282         elif filternum == webifHandler.FILTER_URLENCODE:
283                 append(quote(item))
284         elif filternum == webifHandler.FILTER_DATE:
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 == webifHandler.FILTER_TIME:
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 == webifHandler.FILTER_MINUTES:
303                 time = 0
304                 try:
305                         time = int(item)
306                         append("%d min" % (time / 60))
307                 except:
308                         append("-- min")
309         elif filternum == webifHandler.FILTER_URI_ATTRIB:
310                 append(quote(item).replace("\"", "%22"))
311         elif filternum == webifHandler.FILTER_HTML:
312                 if item == "None":
313                         append("n/a")
314                 else:
315                         append(item.replace("\n", "<br />"))
316         elif filternum == webifHandler.FILTER_ATTRIBUTE:
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         FILTER_NONE = 1
329         FILTER_JAVASCRIPT = 2
330         FILTER_XML = 3
331         FILTER_URI = 4
332         FILTER_URLENCODE = 5
333         FILTER_DATE = 6
334         FILTER_TIME = 7
335         FILTER_MINUTES = 8
336         FILTER_URI_ATTRIB = 9
337         FILTER_HTML = 10
338         FILTER_ATTRIBUTE = 11
339
340         def __init__(self, session, request):
341                 self.res = [ ]
342                 self.mode = 0
343                 self.screen = None
344                 self.session = session
345                 self.screens = [ ]
346                 self.request = request
347                 self.macros = { }
348
349         def start_element(self, attrs):
350                 scr = self.screen
351                 wsource = attrs["source"]
352
353                 path = wsource.split('.')
354                 while len(path) > 1:
355                         scr = self.screen.getRelatedScreen(path[0])
356                         if scr is None:
357                                 print "[webif.py] Parent Screen not found!"
358                                 print wsource
359                         path = path[1:]
360
361                 source = scr.get(path[0])
362
363                 if isinstance(source, ObsoleteSource):
364                         # however, if we found an "obsolete source", issue warning, and resolve the real source.
365                         print "WARNING: WEBIF '%s' USES OBSOLETE SOURCE '%s', USE '%s' INSTEAD!" % (name, wsource, source.new_source)
366                         print "OBSOLETE SOURCE WILL BE REMOVED %s, PLEASE UPDATE!" % (source.removal_date)
367                         if source.description:
368                                 print source.description
369
370                         wsource = source.new_source
371                 else:
372                         pass
373                         # otherwise, use that source.
374
375                 self.source = source
376                 self.source_id = str(attrs.get("id", wsource))
377                 self.is_streaming = "streaming" in attrs
378                 self.macro_name = attrs.get("macro") or None
379
380         def end_element(self):
381                 # instatiate either a StreamingElement or a OneTimeElement, depending on what's required.
382                 if not self.is_streaming:
383                         if self.macro_name is None:
384                                 c = OneTimeElement(self.source_id)
385                         else:
386                                 c = MacroElement(self.source_id, self.macros, self.macro_name)
387                 else:
388                         assert self.macro_name is None
389                         c = StreamingElement(self.source_id)
390
391                 c.connect(self.source)
392                 self.res.append(c)
393                 self.screen.renderer.append(c)
394                 del self.source
395
396         def start_convert(self, attrs):
397                 ctype = attrs["type"]
398
399                 # TODO: we need something better here
400                 if ctype[:4] == "web:": # for now
401                         self.converter = eval(ctype[4:])
402                 else:
403                         try:
404                                 self.converter = my_import('.'.join(("Components", "Converter", ctype))).__dict__.get(ctype)
405                         except ImportError:
406                                 self.converter = my_import('.'.join(("Plugins", "Extensions", "WebInterface", "WebComponents", "Converter", ctype))).__dict__.get(ctype)
407                 self.sub = [ ]
408
409         def end_convert(self):
410                 sub = self.sub
411                 if len(sub) == 1:
412                         sub = sub[0]
413                 c = self.converter(sub)
414                 c.connect(self.source)
415                 self.source = c
416                 del self.sub
417
418         def parse_item(self, attrs):
419                 if "name" in attrs:
420                         filter = {
421                                 ""                                      : webifHandler.FILTER_NONE,
422                                 "javascript_escape"     : webifHandler.FILTER_JAVASCRIPT,
423                                 "xml"                           : webifHandler.FILTER_XML,
424                                 "uri"                           : webifHandler.FILTER_URI,
425                                 "urlencode"                     : webifHandler.FILTER_URLENCODE,
426                                 "date"                          : webifHandler.FILTER_DATE,
427                                 "time"                          : webifHandler.FILTER_TIME,
428                                 "minutes"                       : webifHandler.FILTER_MINUTES,
429                                 "uriAttribute"          : webifHandler.FILTER_URI_ATTRIB,
430                                 "html"                          : webifHandler.FILTER_HTML,
431                                 "attribute"             : webifHandler.FILTER_ATTRIBUTE
432                         }[attrs.get("filter", "")]
433                         self.sub.append(ListItem(attrs["name"], filter))
434                 else:
435                         assert "macro" in attrs, "e2:item must have a name= or macro= attribute!"
436                         self.sub.append(ListMacroItem(self.macros, attrs["macro"]))
437
438         def startElement(self, name, attrs):
439                 if name == "e2:screen":
440                         if "external_module" in attrs:
441                                 exec "from " + attrs["external_module"] + " import *"
442                         self.screen = eval(attrs["name"])(self.session, self.request) # fixme
443                         self.screens.append(self.screen)
444                         return
445
446                 n3 = name[:3]
447                 if n3 == "e2:":
448                         self.mode += 1
449
450                 tag = '<' + name + ''.join([' %s="%s"' % x for x in attrs.items()]) + '>'
451                 #tag = tag.encode('utf-8')
452
453                 if self.mode == 0:
454                         self.res.append(tag)
455                 elif self.mode == 1: # expect "<e2:element>"
456                         assert name == "e2:element", "found %s instead of e2:element" % name
457                         self.start_element(attrs)
458                 elif self.mode == 2: # expect "<e2:convert>"
459                         if n3 == "e2:":
460                                 assert name == "e2:convert"
461                                 self.start_convert(attrs)
462                         else:
463                                 self.sub.append(tag)
464                 elif self.mode == 3:
465                         assert name == "e2:item", "found %s instead of e2:item!" % name
466
467                         self.parse_item(attrs)
468
469         def endElement(self, name):
470                 if name == "e2:screen":
471                         self.screen = None
472                         return
473
474                 n3 = name[:3]
475                 tag = "</" + name + ">"
476                 if self.mode == 0:
477                         self.res.append(tag)
478                 elif self.mode == 2:
479                         if n3 == "e2:": # closed 'convert' -> sub
480                                 self.end_convert()
481                         else:
482                                 self.sub.append(tag)
483                 elif self.mode == 1: # closed 'element'
484                         self.end_element()
485                 if n3 == "e2:":
486                         self.mode -= 1
487
488         def processingInstruction(self, target, data):
489                 self.res.append('<?' + target + ' ' + data + '>')
490
491         def characters(self, ch):
492                 ch = ch.encode('utf-8')
493                 if self.mode == 0:
494                         self.res.append(ch)
495                 elif self.mode == 2:
496                         self.sub.append(ch)
497
498         def startEntity(self, name):
499                 self.res.append('&' + name + ';');
500
501         def execBegin(self):
502                 for screen in self.screens:
503                         screen.execBegin()
504
505         def cleanup(self):
506                 for screen in self.screens:
507                         screen.execEnd()
508                         screen._Screen__doClose()
509                 self.screens = [ ]
510
511 #===============================================================================
512 # renderPage
513 #
514 # Creates the Handler for a Request and calls it
515 # Also ensures that the Handler is finished after the Request is done
516 #===============================================================================
517 def renderPage(request, path, session):
518         # read in the template, create required screens
519         # we don't have persistense yet.
520         # if we had, this first part would only be done once.
521         handler = webifHandler(session, request)
522         parser = make_parser()
523         parser.setFeature(feature_namespaces, 0)
524         parser.setContentHandler(handler)
525         parser.parse(open(util.sibpath(__file__, path)))
526
527         # by default, we have non-streaming pages
528         finish = True
529
530         if config.plugins.Webinterface.anti_hijack.value:
531                 for screen in handler.screens:
532                         method = request.method
533                         if not screen.allow_GET and method == 'GET' and len(request.args) > 0:
534                                 request.setHeader("Allow", "POST")
535                                 request.write(
536                                         resource.ErrorPage(http.NOT_ALLOWED, "Invalid method: GET!", "GET is not allowed here, please use POST").render(request)
537                                 )
538                                 if not request._disconnected:
539                                         request.finish()
540                                 else:
541                                         print "[renderPage] request already finished!"
542                                 return
543
544         # first, apply "commands" (aka. URL argument)
545         for x in handler.res:
546                 if isinstance(x, Element):
547                         x.handleCommand(request.args)
548
549         handler.execBegin()
550
551         # now, we have a list with static texts mixed
552         # with non-static Elements.
553         # flatten this list, write into the request.
554         for x in handler.res:
555                 if isinstance(x, Element):
556                         if isinstance(x, StreamingElement):
557                                 finish = False
558                                 x.setRequest(request)
559                         x.render(request)
560                 else:
561                         request.write(str(x))
562
563         # if we met a "StreamingElement", there is at least one
564         # element which wants to output data more than once,
565         # i.e. on host-originated changes.
566         # in this case, don't finish yet, don't cleanup yet,
567         # but instead do that when the client disconnects.
568
569 # This is some debug code, i'll leave it for possible later use
570 #       def requestFinishedTest(nothing, handler, request):
571 #               print "Request has been cleaned: %s" %request
572 #               print nothing
573 #
574 #       d2 = request.notifyFinish()
575 #       d2.addBoth(requestFinishedTest, handler, request)
576         if finish:
577                 requestFinish(handler, request)
578
579         else:
580                 def _requestFinishDeferred(nothing, handler, request):
581                         from twisted.internet import reactor
582                         reactor.callLater(0, requestFinish, handler, request, requestAlreadyFinished=True)
583
584                 d = request.notifyFinish()
585                 d.addBoth( _requestFinishDeferred, handler, request )
586
587 #===============================================================================
588 # requestFinish
589 #
590 # This has to be/is called at the end of every ScreenPage-based Request
591 #===============================================================================
592 def requestFinish(handler, request, requestAlreadyFinished = False):
593         handler.cleanup()
594         if not requestAlreadyFinished:
595                 try:
596                         if not request._disconnected:
597                                 request.finish()
598                         else:
599                                 print "[requestFinish] request already finished!"
600                 except:
601                         pass
602
603         del handler
604
605 def validate_certificate(cert, key):
606         buf = decrypt_block(cert[8:], key)
607         if buf is None:
608                 return None
609         return buf[36:107] + cert[139:196]
610
611 def get_random():
612         try:
613                 xor = lambda a,b: ''.join(chr(ord(c)^ord(d)) for c,d in zip(a,b*100))
614                 random = urandom(8)
615                 x = str(time())[-8:]
616                 result = xor(random, x)
617
618                 return result
619         except:
620                 return None