Changes:
[vuplus_dvbapp-plugin] / webinterface / src / webif.py
1 # -*- coding: UTF-8 -*-
2 Version = '$Header$';
3
4 # things to improve:
5 #       - nicer code
6 #       - screens need to be defined somehow else.
7 #         I don't know how, yet. Probably each in an own file.
8 #       - better error handling
9 #       - use namespace parser
10
11 from Tools.Import import my_import
12
13 from Components.Sources.Source import ObsoleteSource
14 from Components.Converter.Converter import Converter
15 from Components.Element import Element
16
17 from xml.sax import make_parser
18 from xml.sax.handler import ContentHandler, feature_namespaces
19 from xml.sax.saxutils import escape as escape_xml
20 from twisted.python import util
21 from urllib2 import quote
22
23 #DO NOT REMOVE THIS IMPORT
24 #It IS used (dynamically)
25 from WebScreens import *
26 #DO NOT REMOVE THIS IMPORT
27                 
28 # implements the 'render'-call.
29 # this will act as a downstream_element, like a renderer.
30 class OneTimeElement(Element):
31         def __init__(self, id):
32                 Element.__init__(self)
33                 self.source_id = id
34
35         # CHECKME: is this ok performance-wise?
36         def handleCommand(self, args):
37                 if self.source_id.find(",") >= 0:
38                         paramlist = self.source_id.split(",")
39                         list = {}
40                         for key in paramlist:
41                                 arg = args.get(key, [])
42                                 if len(arg) == 0:
43                                         list[key] = None
44                                 elif len(arg) == 1:
45                                         list[key] = "".join(arg)
46                                 elif len(arg) == 2:
47                                         list[key] = arg[0]
48                         self.source.handleCommand(list)
49                 else:
50                         for c in args.get(self.source_id, []):
51                                 self.source.handleCommand(c)
52
53         def render(self, stream):
54                 t = self.source.getHTML(self.source_id)
55                 stream.write(t)
56
57         def execBegin(self):
58                 self.suspended = False
59
60         def execEnd(self):
61                 self.suspended = True
62
63         def onShow(self):
64                 pass
65
66         def onHide(self):
67                 pass
68
69         def destroy(self):
70                 pass
71
72 class MacroElement(OneTimeElement):
73         def __init__(self, id, macro_dict, macro_name):
74                 OneTimeElement.__init__(self, id)
75                 self.macro_dict = macro_dict
76                 self.macro_name = macro_name
77
78         def render(self, stream):
79                 self.macro_dict[self.macro_name] = self.source.getHTML(self.source_id)
80
81 class StreamingElement(OneTimeElement):
82         def __init__(self, id):
83                 OneTimeElement.__init__(self, id)
84                 self.stream = None
85
86         def changed(self, what):
87                 if self.stream:
88                         self.render(self.stream)
89
90         def setStream(self, stream):
91                 self.stream = stream
92
93 # a to-be-filled list item
94 class ListItem:
95         def __init__(self, name, filternum):
96                 self.name = name
97                 self.filternum = filternum
98
99 class ListMacroItem:
100         def __init__(self, macrodict, macroname):
101                 self.macrodict = macrodict
102                 self.macroname = macroname
103
104 class TextToHTML(Converter):
105         def __init__(self, arg):
106                 Converter.__init__(self, arg)
107
108         def getHTML(self, id):
109                 return self.source.text # encode & etc. here!
110
111 class TextToXML(Converter):
112         def __init__(self, arg):
113                 Converter.__init__(self, arg)
114
115         def getHTML(self, id):
116                 return escape_xml(self.source.text).replace("\x19", "").replace("\x1c", "").replace("\x1e", "")
117
118 class TextToURL(Converter):
119         def __init__(self, arg):
120                 Converter.__init__(self, arg)
121
122         def getHTML(self, id):
123                 return self.source.text.replace(" ", "%20")
124
125 class ReturnEmptyXML(Converter):
126         def __init__(self, arg):
127                 Converter.__init__(self, arg)
128
129         def getHTML(self, id):
130                 return "<rootElement></rootElement>"
131
132 # a null-output. Useful if you only want to issue a command.
133 class Null(Converter):
134         def __init__(self, arg):
135                 Converter.__init__(self, arg)
136
137         def getHTML(self, id):
138                 return ""
139
140 class JavascriptUpdate(Converter):
141         def __init__(self, arg):
142                 Converter.__init__(self, arg)
143
144         def getHTML(self, id):
145                 # 3c5x9, added parent. , this is because the ie loads this in a iframe. an the set is in index.html.xml
146                 #                all other will replace this in JS
147                 return '<script>parent.set("%s", "%s");</script>\n' % (id, self.source.text.replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"').replace('\xb0', '&deg;'))
148
149 # the performant 'one-dimensonial listfiller' engine (podlfe)
150 class SimpleListFiller(Converter):
151         def __init__(self, arg):
152                 Converter.__init__(self, arg)
153                 
154         def getText(self):
155                 l = self.source.simplelist
156                 conv_args = self.converter_arguments            
157                 
158                 list = [ ]
159                 for element in conv_args:
160                         if isinstance(element, basestring):
161                                 list.append((element, None))
162                         elif isinstance(element, ListItem):
163                                 list.append((element, element.filternum))
164                         elif isinstance(element, ListMacroItem):
165                                 list.append(element.macrodict[element.macroname], None)
166                         else:
167                                 raise Exception("neither string, ListItem nor ListMacroItem")
168                         
169                 strlist = [ ]
170                 append = strlist.append
171                 for item in l:
172                         if item is None:
173                                 item = ""
174                                 
175                         for (element, filternum) in list:
176                                 if not filternum:
177                                         append(element)
178                                 elif filternum == 2:
179                                         append(str(item).replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"'))
180                                 elif filternum == 3:                                    
181                                         append(escape_xml(str(item)))
182                                 elif filternum == 4:
183                                         append(str(item).replace("%", "%25").replace("+", "%2B").replace('&', '%26').replace('?', '%3f').replace(' ', '+'))
184                                 elif filternum == 5:
185                                         append(quote(str(item)))
186                                 elif filternum == 6:
187                                         time = parseint(item) or 0
188                                         t = localtime(time)
189                                         append("%02d:%02d" % (t.tm_hour, t.tm_min))
190                                 elif filternum == 7:
191                                         time = parseint(item) or 0
192                                         t = localtime(time)
193                                         append("%d min" % (time / 60))
194                                 else:
195                                         append(str(item))
196                 # (this will be done in c++ later!)
197
198                 return ''.join(strlist)         
199         
200         text = property(getText)
201                 
202                                 
203
204 # the performant 'listfiller'-engine (plfe)
205 class ListFiller(Converter):
206         def __init__(self, arg):
207                 Converter.__init__(self, arg)
208 #               print "ListFiller-arg: ",arg
209
210         def getText(self):
211                 l = self.source.list
212                 lut = self.source.lut
213                 conv_args = self.converter_arguments
214
215                 # now build a ["string", 1, "string", 2]-styled list, with indices into the
216                 # list to avoid lookup of item name for each entry
217                 lutlist = [ ]
218                 for element in conv_args:
219                         if isinstance(element, basestring):
220                                 lutlist.append((element, None))
221                         elif isinstance(element, ListItem):
222                                 lutlist.append((lut[element.name], element.filternum))
223                         elif isinstance(element, ListMacroItem):
224                                 lutlist.append((element.macrodict[element.macroname], None))
225                         else:
226                                 raise Exception("neither string, ListItem nor ListMacroItem")
227
228                 # now, for the huge list, do:
229                 strlist = [ ]
230                 append = strlist.append
231                 for item in l:
232                         for (element, filternum) in lutlist:                    
233                                 #None becomes ""
234                                 curitem = ""
235                                 if filternum:
236                                         curitem = item[element]
237                                         if curitem is None:
238                                                 curitem = ""
239                                 else:
240                                         if element is None:
241                                                 element = ""
242                                                 
243                                 if not filternum:
244                                         append(element)
245                                 elif filternum == 2:
246                                         append(str(curitem).replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"'))
247                                 elif filternum == 3:
248                                         append(escape_xml(str(curitem)))
249                                 elif filternum == 4:
250                                         append(str(curitem).replace("%", "%25").replace("+", "%2B").replace('&', '%26').replace('?', '%3f').replace(' ', '+'))
251                                 elif filternum == 5:
252                                         append(quote(str(curitem)))
253                                 elif filternum == 6:
254                                         from time import localtime
255                                         time = int(float(curitem)) or 0
256                                         t = localtime(time)
257                                         append("%02d:%02d" % (t.tm_hour, t.tm_min))
258                                 elif filternum == 7:
259                                         from time import localtime
260                                         time = int(float(curitem)) or 0
261                                         t = localtime(time)
262                                         append("%d min" % (time / 60))                                  
263                                 else:
264                                         append(str(curitem))
265                 # (this will be done in c++ later!)
266
267                 return ''.join(strlist)
268
269         text = property(getText)
270
271 class webifHandler(ContentHandler):
272         def __init__(self, session, request):
273                 self.res = [ ]
274                 self.mode = 0
275                 self.screen = None
276                 self.session = session
277                 self.screens = [ ]
278                 self.request = request
279                 self.macros = { }
280
281         def start_element(self, attrs):
282                 scr = self.screen
283
284                 wsource = attrs["source"]
285
286                 path = wsource.split('.')
287                 while len(path) > 1:
288                         scr = self.screen.getRelatedScreen(path[0])
289                         if scr is None:
290                                 print "[webif.py] Parent Screen not found!"
291                                 print wsource
292                         path = path[1:]
293
294                 source = scr.get(path[0])
295
296                 if isinstance(source, ObsoleteSource):
297                         # however, if we found an "obsolete source", issue warning, and resolve the real source.
298                         print "WARNING: WEBIF '%s' USES OBSOLETE SOURCE '%s', USE '%s' INSTEAD!" % (name, wsource, source.new_source)
299                         print "OBSOLETE SOURCE WILL BE REMOVED %s, PLEASE UPDATE!" % (source.removal_date)
300                         if source.description:
301                                 print source.description
302
303                         wsource = source.new_source
304                 else:
305                         pass
306                         # otherwise, use that source.
307
308                 self.source = source
309                 self.source_id = str(attrs.get("id", wsource))
310                 self.is_streaming = "streaming" in attrs
311                 self.macro_name = attrs.get("macro") or None
312
313         def end_element(self):
314                 # instatiate either a StreamingElement or a OneTimeElement, depending on what's required.
315                 if not self.is_streaming:
316                         if self.macro_name is None:
317                                 c = OneTimeElement(self.source_id)
318                         else:
319                                 c = MacroElement(self.source_id, self.macros, self.macro_name)
320                 else:
321                         assert self.macro_name is None
322                         c = StreamingElement(self.source_id)
323
324                 c.connect(self.source)
325                 self.res.append(c)
326                 self.screen.renderer.append(c)
327                 del self.source
328
329         def start_convert(self, attrs):
330                 ctype = attrs["type"]
331
332                 # TODO: we need something better here
333                 if ctype[:4] == "web:": # for now
334                         self.converter = eval(ctype[4:])
335                 else:
336                         try:
337                                 self.converter = my_import('.'.join(["Components", "Converter", ctype])).__dict__.get(ctype)
338                         except ImportError:
339                                 self.converter = my_import('.'.join(["Plugins", "Extensions", "WebInterface", "WebComponents", "Converter", ctype])).__dict__.get(ctype)
340                 self.sub = [ ]
341
342         def end_convert(self):
343                 if len(self.sub) == 1:
344                         self.sub = self.sub[0]
345                 c = self.converter(self.sub)
346                 c.connect(self.source)
347                 self.source = c
348                 del self.sub
349
350         def parse_item(self, attrs):
351                 if "name" in attrs:
352                         filter = {"": 1, "javascript_escape": 2, "xml": 3, "uri": 4, "urlencode": 5, "time": 6, "minutes": 7}[attrs.get("filter", "")]
353                         self.sub.append(ListItem(attrs["name"], filter))
354                 else:
355                         assert "macro" in attrs, "e2:item must have a name= or macro= attribute!"
356                         self.sub.append(ListMacroItem(self.macros, attrs["macro"]))
357
358         def startElement(self, name, attrs):
359                 if name == "e2:screen":
360                         self.screen = eval(attrs["name"])(self.session, self.request) # fixme
361                         self.screens.append(self.screen)
362                         return
363
364                 if name[:3] == "e2:":
365                         self.mode += 1
366
367                 tag = [' %s="%s"' % (key, val) for (key, val) in attrs.items()]
368                 tag.insert(0, name)
369                 tag.insert(0, '<')
370                 tag.append('>')
371                 tag = ''.join(tag)#.encode('utf-8')
372
373                 if self.mode == 0:
374                         self.res.append(tag)
375                 elif self.mode == 1: # expect "<e2:element>"
376                         assert name == "e2:element", "found %s instead of e2:element" % name
377                         self.start_element(attrs)
378                 elif self.mode == 2: # expect "<e2:convert>"
379                         if name[:3] == "e2:":
380                                 assert name == "e2:convert"
381                                 self.start_convert(attrs)
382                         else:
383                                 self.sub.append(tag)
384                 elif self.mode == 3:
385                         assert name == "e2:item", "found %s instead of e2:item!" % name
386
387                         self.parse_item(attrs)
388
389         def endElement(self, name):
390                 if name == "e2:screen":
391                         self.screen = None
392                         return
393
394                 tag = "</" + name + ">"
395                 if self.mode == 0:
396                         self.res.append(tag)
397                 elif self.mode == 2 and name[:3] != "e2:":
398                         self.sub.append(tag)
399                 elif self.mode == 2: # closed 'convert' -> sub
400                         self.end_convert()
401                 elif self.mode == 1: # closed 'element'
402                         self.end_element()
403                 if name[:3] == "e2:":
404                         self.mode -= 1
405
406         def processingInstruction(self, target, data):
407                 self.res.append('<?' + target + ' ' + data + '>')
408
409         def characters(self, ch):
410                 ch = ch.encode('utf-8')
411                 if self.mode == 0:
412                         self.res.append(ch)
413                 elif self.mode == 2:
414                         self.sub.append(ch)
415
416         def startEntity(self, name):
417                 self.res.append('&' + name + ';');
418
419         def execBegin(self):
420                 for screen in self.screens:
421                         screen.execBegin()
422
423         def cleanup(self):
424                 print "screen cleanup!"
425                 for screen in self.screens:
426                         screen.execEnd()
427                         screen.doClose()
428                 self.screens = [ ]
429
430 def renderPage(stream, path, req, session):
431         # read in the template, create required screens
432         # we don't have persistense yet.
433         # if we had, this first part would only be done once.
434         handler = webifHandler(session, req)
435         parser = make_parser()
436         parser.setFeature(feature_namespaces, 0)
437         parser.setContentHandler(handler)
438         parser.parse(open(util.sibpath(__file__, path)))
439
440         # by default, we have non-streaming pages
441         finish = True
442
443         # first, apply "commands" (aka. URL argument)
444         for x in handler.res:
445                 if isinstance(x, Element):
446                         x.handleCommand(req.args)
447
448         handler.execBegin()
449
450         # now, we have a list with static texts mixed
451         # with non-static Elements.
452         # flatten this list, write into the stream.
453         for x in handler.res:
454                 if isinstance(x, Element):
455                         if isinstance(x, StreamingElement):
456                                 finish = False
457                                 x.setStream(stream)
458                         x.render(stream)
459                 else:
460                         stream.write(str(x))
461
462         def ping(s):
463                 from twisted.internet import reactor
464                 s.write("\n");
465                 reactor.callLater(3, ping, s)
466
467         # if we met a "StreamingElement", there is at least one
468         # element which wants to output data more than once,
469         # i.e. on host-originated changes.
470         # in this case, don't finish yet, don't cleanup yet,
471         # but instead do that when the client disconnects.
472         if finish:
473                 streamFinish(handler, stream)
474         else:
475                 # ok.
476                 # you *need* something which constantly sends something in a regular interval,
477                 # in order to detect disconnected clients.
478                 # i agree that this "ping" sucks terrible, so better be sure to have something
479                 # similar. A "CurrentTime" is fine. Or anything that creates *some* output.
480                 ping(stream)
481                 stream.closed_callback = lambda : streamFinish(handler, stream)
482
483 def streamFinish(handler, stream):
484         handler.cleanup()
485         stream.finish()
486         del handler
487         del stream