* proper escape xml special chars
[enigma2-plugins.git] / webinterface / src / webif.py
1 Version = '$Header$';
2
3 # OK, this is more than a proof of concept
4 # things to improve:
5 #  - nicer code
6 #  - screens need to be defined somehow else. 
7 #    I don't know how, yet. Probably each in an own file.
8 #  - more components, like the channellist
9 #  - better error handling
10 #  - use namespace parser
11
12 from Screens.Screen import Screen
13 from Tools.Import import my_import
14
15 # for our testscreen
16 from Screens.InfoBarGenerics import InfoBarServiceName, InfoBarEvent, InfoBarTuner
17
18 from Components.Sources.Clock import Clock
19 from Components.Sources.ServiceList import ServiceList
20
21 from WebComponents.Sources.ServiceListRecursive import ServiceListRecursive
22 from WebComponents.Sources.Volume import Volume
23 from WebComponents.Sources.EPG import EPG
24 from WebComponents.Sources.Timer import Timer
25 from WebComponents.Sources.Movie import Movie
26 from WebComponents.Sources.Message import Message
27 from WebComponents.Sources.PowerState import PowerState
28 from WebComponents.Sources.RemoteControl import RemoteControl
29 from WebComponents.Sources.Settings import Settings
30 from WebComponents.Sources.SubServices import SubServices
31 from WebComponents.Sources.ParentControl import ParentControl
32 from WebComponents.Sources.About import About
33 from WebComponents.Sources.RequestData import RequestData
34
35 from Components.Sources.FrontendStatus import FrontendStatus
36
37 from Components.Converter.Converter import Converter
38
39 from Components.Element import Element
40
41 from xml.sax import make_parser
42 from xml.sax.handler import ContentHandler, feature_namespaces
43
44 from twisted.python import util
45
46 import sys
47 import time
48  
49 # prototype of the new web frontend template system.
50
51 class WebScreen(Screen):
52         def __init__(self, session, request):
53                 Screen.__init__(self, session)
54                 self.stand_alone = True
55                 self.request = request
56                 self.instance = None
57                 
58 # a test screen
59 class TestScreen(InfoBarServiceName, InfoBarEvent,InfoBarTuner, WebScreen):
60         def __init__(self, session,request):
61                 WebScreen.__init__(self, session,request)
62                 InfoBarServiceName.__init__(self)
63                 InfoBarEvent.__init__(self)
64                 InfoBarTuner.__init__(self)
65                 self["CurrentTime"] = Clock()
66 #               self["TVSystem"] = Config(config.av.tvsystem)
67 #               self["OSDLanguage"] = Config(config.osd.language)
68 #               self["FirstRun"] = Config(config.misc.firstrun)
69                 from enigma import eServiceReference
70                 fav = eServiceReference('1:7:1:0:0:0:0:0:0:0:(type == 1) || (type == 17) || (type == 195) || (type == 25) FROM BOUQUET "bouquets.tv" ORDER BY bouquet')
71                 self["SwitchService"] = ServiceList(fav, command_func = self.zapTo, validate_commands=False)
72                 self["ServiceList"] = ServiceList(fav, command_func = self.getServiceList, validate_commands=False)
73                 self["ServiceListRecursive"] = ServiceListRecursive(session, func=ServiceListRecursive.FETCH)
74                 self["ParentControlList"] = ParentControl(session)
75                 self["SubServices"] = SubServices(session)
76                 self["Volume"] = Volume(session)
77                 self["EPGTITLE"] = EPG(session,func=EPG.TITLE)
78                 self["EPGSERVICE"] = EPG(session,func=EPG.SERVICE)
79                 self["EPGNOW"] = EPG(session,func=EPG.NOW)
80                 self["TimerList"] = Timer(session,func = Timer.LIST)
81                 self["TimerAddEventID"] = Timer(session,func = Timer.ADDBYID)
82                 self["TimerAdd"] = Timer(session,func = Timer.ADD)
83                 self["TimerDel"] = Timer(session,func = Timer.DEL)
84                 self["TimerChange"] = Timer(session,func = Timer.CHANGE)
85                 self["TimerListWrite"] = Timer(session,func = Timer.WRITE)
86                 self["TVBrowser"] = Timer(session,func = Timer.TVBROWSER)
87                 self["RecordNow"] = Timer(session,func = Timer.RECNOW)
88                 self["MovieList"] = Movie(session,func = Movie.LIST)
89                 self["MovieFileDel"] = Movie(session,func = Movie.DEL)
90                 self["Volume"] = Volume(session)
91                 self["Message"] = Message(session)
92                 self["PowerState"] = PowerState(session)
93                 self["RemoteControl"] = RemoteControl(session)
94                 self["Settings"] = Settings(session)
95                 
96                 self["About"] = About(session)
97                 
98         def getServiceList(self, sRef):
99                 self["ServiceList"].root = sRef
100
101         def zapTo(self, reftozap):
102                 from Components.config import config
103                 pc = config.ParentalControl.configured.value
104                 if pc:
105                         config.ParentalControl.configured.value = False
106                 self.session.nav.playService(reftozap)
107                 if pc:
108                         config.ParentalControl.configured.value = pc
109                 """
110                 switching config.ParentalControl.configured.value
111                 ugly, but necessary :(
112                 """
113
114 # TODO: (really.) put screens into own files.
115 class Streaming(WebScreen):
116         def __init__(self, session,request):
117                 WebScreen.__init__(self, session,request)
118                 from Components.Sources.StreamService import StreamService
119                 self["StreamService"] = StreamService(self.session.nav)
120
121 class StreamingM3U(WebScreen):
122         def __init__(self, session,request):
123                 WebScreen.__init__(self, session,request)
124                 from Components.Sources.StaticText import StaticText
125                 from Components.Sources.Config import Config
126                 from Components.config import config
127                 self["ref"] = StaticText()
128                 self["localip"] = RequestData(request,what=RequestData.HOST)
129
130 class TsM3U(WebScreen):
131         def __init__(self, session,request):
132                 WebScreen.__init__(self, session,request)
133                 from Components.Sources.StaticText import StaticText
134                 from Components.Sources.Config import Config
135                 from Components.config import config
136                 self["file"] = StaticText()
137                 self["localip"] = RequestData(request,what=RequestData.HOST)
138
139 class RestartTwisted(WebScreen):
140         def __init__(self, session,request):
141                 WebScreen.__init__(self, session,request)
142                 import plugin
143                 plugin.restartWebserver()
144                 
145 class GetPid(WebScreen):
146       def __init__(self, session,request):
147          WebScreen.__init__(self, session,request)
148          from Components.Sources.StaticText import StaticText
149          from enigma import iServiceInformation
150          pids = self.session.nav.getCurrentService()
151          if pids is not None:
152                  pidinfo = pids.info()
153                  VPID = hex(pidinfo.getInfo(iServiceInformation.sVideoPID))
154                  APID = hex(pidinfo.getInfo(iServiceInformation.sAudioPID))
155                  PPID = hex(pidinfo.getInfo(iServiceInformation.sPMTPID))
156          self["pids"] = StaticText("%s,%s,%s"%(PPID.lstrip("0x"),VPID.lstrip("0x"),APID.lstrip("0x")))
157          self["localip"] = RequestData(request,what=RequestData.HOST)
158
159
160 # implements the 'render'-call.
161 # this will act as a downstream_element, like a renderer.
162 class OneTimeElement(Element):
163         def __init__(self, id):
164                 Element.__init__(self)
165                 self.source_id = id
166
167         # CHECKME: is this ok performance-wise?
168         def handleCommand(self, args):
169                 if self.source_id.find(",") >=0:
170                         paramlist = self.source_id.split(",")
171                         list={}
172                         for key in paramlist:
173                                 arg = args.get(key, [])
174                                 if len(arg) == 0:
175                                         list[key] = None        
176                                 elif len(arg) == 1:
177                                         list[key] = "".join(arg)        
178                                 elif len(arg) == 2:
179                                         list[key] = arg[0]
180                         self.source.handleCommand(list)
181                 else:
182                         for c in args.get(self.source_id, []):
183                                 self.source.handleCommand(c)
184                 
185         def render(self, stream):
186                 t = self.source.getHTML(self.source_id)
187                 stream.write(t)
188
189         def execBegin(self):
190                 pass
191         
192         def execEnd(self):
193                 pass
194         
195         def onShow(self):
196                 pass
197
198         def onHide(self):
199                 pass
200         
201         def destroy(self):
202                 pass
203
204 class StreamingElement(OneTimeElement):
205         def __init__(self, id):
206                 OneTimeElement.__init__(self, id)
207                 self.stream = None
208
209         def changed(self, what):
210                 if self.stream:
211                         self.render(self.stream)
212
213         def setStream(self, stream):
214                 self.stream = stream
215
216 # a to-be-filled list item
217 class ListItem:
218         def __init__(self, name, filternum):
219                 self.name = name
220                 self.filternum = filternum
221         
222 class TextToHTML(Converter):
223         def __init__(self, arg):
224                 Converter.__init__(self, arg)
225
226         def getHTML(self, id):
227                 return self.source.text # encode & etc. here!
228
229 class ReturnEmptyXML(Converter):
230         def __init__(self, arg):
231                 Converter.__init__(self, arg)
232                 
233         def getHTML(self, id):
234                 return "<rootElement></rootElement>"
235
236 # a null-output. Useful if you only want to issue a command.
237 class Null(Converter):
238         def __init__(self, arg):
239                 Converter.__init__(self, arg)
240
241         def getHTML(self, id):
242                 return ""
243
244 class JavascriptUpdate(Converter):
245         def __init__(self, arg):
246                 Converter.__init__(self, arg)
247
248         def getHTML(self, id):
249                 # 3c5x9, added parent. , this is because the ie loads this in a iframe. an the set is in index.html.xml
250                 #                all other will replace this in JS
251                 return '<script>parent.set("%s", "%s");</script>\n'%(id, self.source.text.replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"').replace('\xb0', '&deg;'))
252
253 # the performant 'listfiller'-engine (plfe)
254 class ListFiller(Converter):
255         def __init__(self, arg):
256                 Converter.__init__(self, arg)
257
258         def getText(self):
259                 l = self.source.list
260                 lut = self.source.lut
261                 conv_args = self.converter_arguments
262
263                 # now build a ["string", 1, "string", 2]-styled list, with indices into the
264                 # list to avoid lookup of item name for each entry
265                 lutlist = [ isinstance(element, basestring) and (element, None) or (lut[element.name], element.filternum) for element in conv_args ]
266
267                 # now, for the huge list, do:
268                 strlist = [ ]
269                 append = strlist.append
270                 for item in l:
271                         for (element, filternum) in lutlist:
272                                 if not filternum:
273                                         append(element)
274                                 elif filternum == 2:
275                                         append(str(item[element]).replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"'))
276                                 elif filternum == 3:
277                                         append(str(item[element]).replace("&", "&amp;").replace("<", "&lt;").replace('"', '&quot;').replace(">", "&gt;"))
278                                 elif filternum == 4:
279                                         append(str(item[element]).replace("%", "%25").replace("+", "%2B").replace('&', '%26').replace('?', '%3f').replace(' ', '+'))
280                                 else:
281                                         append(str(item[element]))
282                 # (this will be done in c++ later!)
283                 return ''.join(strlist)
284
285         text = property(getText)
286
287 class webifHandler(ContentHandler):
288         def __init__(self, session,request):
289                 self.res = [ ]
290                 self.mode = 0
291                 self.screen = None
292                 self.session = session
293                 self.screens = [ ]
294                 self.request = request
295         
296         def startElement(self, name, attrs):
297                 if name == "e2:screen":
298                         self.screen = eval(attrs["name"])(self.session,self.request) # fixme
299                         self.screens.append(self.screen)
300                         return
301         
302                 if name[:3] == "e2:":
303                         self.mode += 1
304
305                 tag = [' %s="%s"' %(key,val) for (key, val) in attrs.items()]
306                 tag.insert(0, name)
307                 tag.insert(0, '<')
308                 tag.append('>')
309                 tag = ''.join(tag)#.encode('utf-8')
310
311                 if self.mode == 0:
312                         self.res.append(tag)
313                 elif self.mode == 1: # expect "<e2:element>"
314                         assert name == "e2:element", "found %s instead of e2:element" % name
315                         source = attrs["source"]
316                         self.source_id = str(attrs.get("id", source))
317                         self.source = self.screen[source]
318                         self.is_streaming = "streaming" in attrs
319                 elif self.mode == 2: # expect "<e2:convert>"
320                         if name[:3] == "e2:":
321                                 assert name == "e2:convert"
322                                 
323                                 ctype = attrs["type"]
324                                 
325                                         # TODO: we need something better here
326                                 if ctype[:4] == "web:": # for now
327                                         self.converter = eval(ctype[4:])
328                                 else:
329                                         try:
330                                                 self.converter = my_import('.'.join(["Components", "Converter", ctype])).__dict__.get(ctype)
331                                         except ImportError:
332                                                 self.converter = my_import('.'.join(["Plugins", "Extensions", "WebInterface", "WebComponents", "Converter", ctype])).__dict__.get(ctype)
333                                 self.sub = [ ]
334                         else:
335                                 self.sub.append(tag)
336                 elif self.mode == 3:
337                         assert name == "e2:item", "found %s instead of e2:item!" % name
338                         assert "name" in attrs, "e2:item must have a name= attribute!"
339                         filter = {"": 1, "javascript_escape": 2, "xml": 3, "uri": 4}[attrs.get("filter", "")]
340                         self.sub.append(ListItem(attrs["name"], filter))
341
342         def endElement(self, name):
343                 if name == "e2:screen":
344                         self.screen = None
345                         return
346
347                 tag = "</" + name + ">"
348                 if self.mode == 0:
349                         self.res.append(tag)
350                 elif self.mode == 2 and name[:3] != "e2:":
351                         self.sub.append(tag)
352                 elif self.mode == 2: # closed 'convert' -> sub
353                         if len(self.sub) == 1:
354                                 self.sub = self.sub[0]
355                         c = self.converter(self.sub)
356                         c.connect(self.source)
357                         self.source = c
358                         del self.sub
359                 elif self.mode == 1: # closed 'element'
360                         # instatiate either a StreamingElement or a OneTimeElement, depending on what's required.
361                         if not self.is_streaming:
362                                 c = OneTimeElement(self.source_id)
363                         else:
364                                 c = StreamingElement(self.source_id)
365                         
366                         c.connect(self.source)
367                         self.res.append(c)
368                         self.screen.renderer.append(c)
369                         del self.source
370
371                 if name[:3] == "e2:":
372                         self.mode -= 1
373
374         def processingInstruction(self, target, data):
375                 self.res.append('<?' + target + ' ' + data + '>')
376         
377         def characters(self, ch):
378                 ch = ch.encode('utf-8')
379                 if self.mode == 0:
380                         self.res.append(ch)
381                 elif self.mode == 2:
382                         self.sub.append(ch)
383         
384         def startEntity(self, name):
385                 self.res.append('&' + name + ';');
386
387         def execBegin(self):
388                 for screen in self.screens:
389                         screen.execBegin()
390
391         def cleanup(self):
392                 print "screen cleanup!"
393                 for screen in self.screens:
394                         screen.execEnd()
395                         screen.doClose()
396                 self.screens = [ ]
397
398 def renderPage(stream, path, req, session):
399         
400         # read in the template, create required screens
401         # we don't have persistense yet.
402         # if we had, this first part would only be done once.
403         handler = webifHandler(session,req)
404         parser = make_parser()
405         parser.setFeature(feature_namespaces, 0)
406         parser.setContentHandler(handler)
407         print "__file__: ",__file__
408         print "path: ",path
409         parser.parse(open(util.sibpath(__file__, path)))
410         
411         # by default, we have non-streaming pages
412         finish = True
413         
414         # first, apply "commands" (aka. URL argument)
415         for x in handler.res:
416                 if isinstance(x, Element):
417                         x.handleCommand(req.args)
418
419         handler.execBegin()
420
421         # now, we have a list with static texts mixed
422         # with non-static Elements.
423         # flatten this list, write into the stream.
424         for x in handler.res:
425                 if isinstance(x, Element):
426                         if isinstance(x, StreamingElement):
427                                 finish = False
428                                 x.setStream(stream)
429                         x.render(stream)
430                 else:
431                         stream.write(str(x))
432
433         def ping(s):
434                 from twisted.internet import reactor
435                 s.write("\n");
436                 reactor.callLater(3, ping, s)
437         
438         # if we met a "StreamingElement", there is at least one
439         # element which wants to output data more than once,
440         # i.e. on host-originated changes.
441         # in this case, don't finish yet, don't cleanup yet,
442         # but instead do that when the client disconnects.
443         if finish:
444                 handler.cleanup()
445                 stream.finish()
446         else:
447                 # ok.
448                 # you *need* something which constantly sends something in a regular interval,
449                 # in order to detect disconnected clients.
450                 # i agree that this "ping" sucks terrible, so better be sure to have something 
451                 # similar. A "CurrentTime" is fine. Or anything that creates *some* output.
452                 ping(stream)
453                 stream.closed_callback = lambda: handler.cleanup()