add Message send to Menu
[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 Components.Sources.FrontendStatus import FrontendStatus
25
26 from Components.Converter.Converter import Converter
27
28 from Components.Element import Element
29
30 from xml.sax import make_parser
31 from xml.sax.handler import ContentHandler, feature_namespaces
32 from twisted.python import util
33 import sys
34 import time
35  
36 # prototype of the new web frontend template system.
37
38 class WebScreen(Screen):
39         def __init__(self, session):
40                 Screen.__init__(self, session)
41                 self.stand_alone = True
42
43 # a test screen
44 class TestScreen(InfoBarServiceName, InfoBarEvent,InfoBarTuner, WebScreen):
45         def __init__(self, session):
46                 WebScreen.__init__(self, session)
47                 InfoBarServiceName.__init__(self)
48                 InfoBarEvent.__init__(self)
49                 InfoBarTuner.__init__(self)
50                 self["CurrentTime"] = Clock()
51 #               self["TVSystem"] = Config(config.av.tvsystem)
52 #               self["OSDLanguage"] = Config(config.osd.language)
53 #               self["FirstRun"] = Config(config.misc.firstrun)
54                 from enigma import eServiceReference
55                 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')
56                 self["ServiceList"] = ServiceList(fav, command_func = self.zapTo, validate_commands=False)
57                 self["ServiceListBrowse"] = ServiceList(fav, command_func = self.browseTo, validate_commands=False)
58                 self["Volume"] = Volume(session)
59                 self["EPGTITLE"] = EPG(session,func=EPG.TITLE)
60                 self["EPGSERVICE"] = EPG(session,func=EPG.SERVICE)
61                 self["EPGNOW"] = EPG(session,func=EPG.NOW)
62                 self["TimerList"] = Timer(session,func = Timer.LIST)
63                 self["TimerAddEventID"] = Timer(session,func = Timer.ADDBYID)
64                 self["TimerAdd"] = Timer(session,func = Timer.ADD)
65                 self["TimerDel"] = Timer(session,func = Timer.DEL)
66                 self["MovieList"] = Movie(session)
67                 self["Volume"] = Volume(session)
68                 self["Message"] = Message(session)
69
70         def browseTo(self, reftobrowse):
71                 self["ServiceListBrowse"].root = reftobrowse
72
73         def zapTo(self, reftozap):
74                 self.session.nav.playService(reftozap)
75
76 # TODO: (really.) put screens into own files.
77 class Streaming(WebScreen):
78         def __init__(self, session):
79                 WebScreen.__init__(self, session)
80                 from Components.Sources.StreamService import StreamService
81                 self["StreamService"] = StreamService(self.session.nav)
82
83 class StreamingM3U(WebScreen):
84         def __init__(self, session):
85                 WebScreen.__init__(self, session)
86                 from Components.Sources.StaticText import StaticText
87                 from Components.Sources.Config import Config
88                 from Components.config import config
89                 
90                 self["ref"] = StaticText()
91                 self["localip"] = Config(config.network.ip)
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):
215                 self.res = [ ]
216                 self.mode = 0
217                 self.screen = None
218                 self.session = session
219                 self.screens = [ ]
220         
221         def startElement(self, name, attrs):
222                 if name == "e2:screen":
223                         self.screen = eval(attrs["name"])(self.session) # fixme
224                         self.screens.append(self.screen)
225                         return
226         
227                 if name[:3] == "e2:":
228                         self.mode += 1
229
230                 tag = [' %s="%s"' %(key,val) for (key, val) in attrs.items()]
231                 tag.insert(0, name)
232                 tag.insert(0, '<')
233                 tag.append('>')
234                 tag = ''.join(tag)#.encode('utf-8')
235
236                 if self.mode == 0:
237                         self.res.append(tag)
238                 elif self.mode == 1: # expect "<e2:element>"
239                         assert name == "e2:element", "found %s instead of e2:element" % name
240                         source = attrs["source"]
241                         self.source_id = str(attrs.get("id", source))
242                         self.source = self.screen[source]
243                         self.is_streaming = "streaming" in attrs
244                 elif self.mode == 2: # expect "<e2:convert>"
245                         if name[:3] == "e2:":
246                                 assert name == "e2:convert"
247                                 
248                                 ctype = attrs["type"]
249                                 
250                                         # TODO: we need something better here
251                                 if ctype[:4] == "web:": # for now
252                                         self.converter = eval(ctype[4:])
253                                 else:
254                                         try:
255                                                 self.converter = my_import('.'.join(["Components", "Converter", ctype])).__dict__.get(ctype)
256                                         except ImportError:
257                                                 self.converter = my_import('.'.join(["Plugins", "Extensions", "WebInterface", "WebComponents", "Converter", ctype])).__dict__.get(ctype)
258                                 self.sub = [ ]
259                         else:
260                                 self.sub.append(tag)
261                 elif self.mode == 3:
262                         assert name == "e2:item", "found %s instead of e2:item!" % name
263                         assert "name" in attrs, "e2:item must have a name= attribute!"
264                         filter = {"": 1, "javascript_escape": 2, "xml": 3, "uri": 4}[attrs.get("filter", "")]
265                         self.sub.append(ListItem(attrs["name"], filter))
266
267         def endElement(self, name):
268                 if name == "e2:screen":
269                         self.screen = None
270                         return
271
272                 tag = "</" + name + ">"
273                 if self.mode == 0:
274                         self.res.append(tag)
275                 elif self.mode == 2 and name[:3] != "e2:":
276                         self.sub.append(tag)
277                 elif self.mode == 2: # closed 'convert' -> sub
278                         if len(self.sub) == 1:
279                                 self.sub = self.sub[0]
280                         c = self.converter(self.sub)
281                         c.connect(self.source)
282                         self.source = c
283                         del self.sub
284                 elif self.mode == 1: # closed 'element'
285                         # instatiate either a StreamingElement or a OneTimeElement, depending on what's required.
286                         if not self.is_streaming:
287                                 c = OneTimeElement(self.source_id)
288                         else:
289                                 c = StreamingElement(self.source_id)
290                         
291                         c.connect(self.source)
292                         self.res.append(c)
293                         self.screen.renderer.append(c)
294                         del self.source
295
296                 if name[:3] == "e2:":
297                         self.mode -= 1
298
299         def processingInstruction(self, target, data):
300                 self.res.append('<?' + target + ' ' + data + '>')
301         
302         def characters(self, ch):
303                 ch = ch.encode('utf-8')
304                 if self.mode == 0:
305                         self.res.append(ch)
306                 elif self.mode == 2:
307                         self.sub.append(ch)
308         
309         def startEntity(self, name):
310                 self.res.append('&' + name + ';');
311
312         def execBegin(self):
313                 for screen in self.screens:
314                         screen.execBegin()
315
316         def cleanup(self):
317                 print "screen cleanup!"
318                 for screen in self.screens:
319                         screen.execEnd()
320                         screen.doClose()
321                 self.screens = [ ]
322
323 def renderPage(stream, path, req, session):
324         
325         # read in the template, create required screens
326         # we don't have persistense yet.
327         # if we had, this first part would only be done once.
328         handler = webifHandler(session)
329         parser = make_parser()
330         parser.setFeature(feature_namespaces, 0)
331         parser.setContentHandler(handler)
332         parser.parse(open(util.sibpath(__file__, path)))
333         
334         # by default, we have non-streaming pages
335         finish = True
336         
337         # first, apply "commands" (aka. URL argument)
338         for x in handler.res:
339                 if isinstance(x, Element):
340                         x.handleCommand(req.args)
341
342         handler.execBegin()
343
344         # now, we have a list with static texts mixed
345         # with non-static Elements.
346         # flatten this list, write into the stream.
347         for x in handler.res:
348                 if isinstance(x, Element):
349                         if isinstance(x, StreamingElement):
350                                 finish = False
351                                 x.setStream(stream)
352                         x.render(stream)
353                 else:
354                         stream.write(str(x))
355
356         def ping(s):
357                 from twisted.internet import reactor
358                 s.write("\n");
359                 reactor.callLater(3, ping, s)
360
361         # if we met a "StreamingElement", there is at least one
362         # element which wants to output data more than once,
363         # i.e. on host-originated changes.
364         # in this case, don't finish yet, don't cleanup yet,
365         # but instead do that when the client disconnects.
366         if finish:
367                 handler.cleanup()
368                 stream.finish()
369         else:
370                 # ok.
371                 # you *need* something which constantly sends something in a regular interval,
372                 # in order to detect disconnected clients.
373                 # i agree that this "ping" sucks terrible, so better be sure to have something 
374                 # similar. A "CurrentTime" is fine. Or anything that creates *some* output.
375                 ping(stream)
376                 stream.closed_callback = lambda: handler.cleanup()