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