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