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