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