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