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