Fix most of the exceptions when no argument given but one needed
[vuplus_dvbapp-plugin] / webinterface / src / webif.py
1 Version = '$Header$';
2
3 # OK, this is more than a proof of concept
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 #  - more components, like the channellist
9 #  - better error handling
10 #  - use namespace parser
11 from enigma import eServiceReference
12
13 from Screens.Screen import Screen
14 from Screens.ChannelSelection import service_types_tv, service_types_radio
15 from Tools.Import import my_import
16
17 from Components.Sources.Source import ObsoleteSource
18
19 from Components.Sources.Clock import Clock
20 from Components.Sources.ServiceList import ServiceList
21
22 from WebComponents.Sources.ServiceListRecursive import ServiceListRecursive
23 from WebComponents.Sources.Volume import Volume
24 from WebComponents.Sources.EPG import EPG
25 from WebComponents.Sources.Timer import Timer
26 from WebComponents.Sources.Movie import Movie
27 from WebComponents.Sources.Message import Message
28 from WebComponents.Sources.PowerState import PowerState
29 from WebComponents.Sources.RemoteControl import RemoteControl
30 from WebComponents.Sources.Settings import Settings
31 from WebComponents.Sources.SubServices import SubServices
32 from WebComponents.Sources.ParentControl import ParentControl
33 from WebComponents.Sources.About import About
34 from WebComponents.Sources.RequestData import RequestData
35 from WebComponents.Sources.AudioTracks import AudioTracks
36 from WebComponents.Sources.WAPfunctions import WAPfunctions
37 from WebComponents.Sources.MP import MP
38 from WebComponents.Sources.Files import Files
39 from WebComponents.Sources.ServiceListReload import ServiceListReload
40 from WebComponents.Sources.AT import AT
41
42 from Components.Sources.FrontendStatus import FrontendStatus
43
44 from Components.Converter.Converter import Converter
45
46 from Components.Element import Element
47
48 from xml.sax import make_parser
49 from xml.sax.handler import ContentHandler, feature_namespaces
50 from xml.sax.saxutils import escape as escape_xml
51 from twisted.python import util
52
53 # prototype of the new web frontend template system.
54
55 class WebScreen(Screen):
56         def __init__(self, session, request):
57                 Screen.__init__(self, session)
58                 self.stand_alone = True
59                 self.request = request
60                 self.instance = None
61
62 class DummyWebScreen(WebScreen):
63         #use it, if you dont need any source, just to can do a static file with an xml-file
64         def __init__(self, session, request):
65                 WebScreen.__init__(self, session, request)
66
67 class UpdateWebScreen(WebScreen):
68         def __init__(self, session, request):
69                 WebScreen.__init__(self, session, request)
70                 self["CurrentTime"] = Clock()
71                 fav = eServiceReference(service_types_tv + ' FROM BOUQUET "bouquets.tv" ORDER BY bouquet')
72
73 class MessageWebScreen(WebScreen):
74         def __init__(self, session, request):
75                 WebScreen.__init__(self, session, request)
76                 self["Message"] = Message(session,func = Message.PRINT)
77                 self["GetAnswer"] = Message(session,func = Message.ANSWER)
78                 
79 class ServiceListReloadWebScreen(WebScreen):
80         def __init__(self, session, request):
81                 WebScreen.__init__(self, session, request)
82                 self["ServiceListReload"] = ServiceListReload(session)
83                 
84 class AudioWebScreen(WebScreen):
85         def __init__(self, session, request):
86                 WebScreen.__init__(self, session, request)
87                 self["AudioTracks"] = AudioTracks(session, func=AudioTracks.GET)
88                 self["SelectAudioTrack"] = AudioTracks(session, func=AudioTracks.SET)   
89
90 class AboutWebScreen(WebScreen):
91         def __init__(self, session, request):
92                 WebScreen.__init__(self, session, request)
93                 self["About"] = About(session)
94                 
95 class VolumeWebScreen(WebScreen):
96         def __init__(self, session, request):
97                 WebScreen.__init__(self, session, request)
98                 self["Volume"] = Volume(session)
99
100 class SettingsWebScreen(WebScreen):
101         def __init__(self, session, request):
102                 WebScreen.__init__(self, session, request)
103                 self["Settings"] = Settings(session)
104
105 class SubServiceWebScreen(WebScreen):
106         def __init__(self, session, request):
107                 WebScreen.__init__(self, session, request)
108                 self["SubServices"] = SubServices(session)
109
110 class ServiceWebScreen(WebScreen):
111         def __init__(self, session, request):
112                 WebScreen.__init__(self, session, request)
113
114                 fav = eServiceReference(service_types_tv + ' FROM BOUQUET "bouquets.tv" ORDER BY bouquet')
115                 self["SwitchService"] = ServiceList(fav, command_func = self.zapTo, validate_commands=False)
116                 self["ServiceList"] = ServiceList(fav, command_func = self.getServiceList, validate_commands=False)
117                 self["ServiceListRecursive"] = ServiceListRecursive(session, func=ServiceListRecursive.FETCH)
118
119         def getServiceList(self, sRef):
120                 self["ServiceList"].root = sRef
121
122         def zapTo(self, reftozap):
123                 from Components.config import config
124                 pc = config.ParentalControl.configured.value
125                 if pc:
126                         config.ParentalControl.configured.value = False
127                 if config.plugins.Webinterface.allowzapping.value:
128                         self.session.nav.playService(reftozap)
129                 if pc:
130                         config.ParentalControl.configured.value = pc
131                 """
132                 switching config.ParentalControl.configured.value
133                 ugly, but necessary :(
134                 """
135
136 class EPGWebScreen(WebScreen):
137         def __init__(self, session, request):
138                 WebScreen.__init__(self, session, request)
139                 self["EPGTITLE"] = EPG(session,func=EPG.TITLE)
140                 self["EPGSERVICE"] = EPG(session,func=EPG.SERVICE)
141                 self["EPGNOW"] = EPG(session,func=EPG.NOW)
142                 self["EPGNEXT"] = EPG(session,func=EPG.NEXT)
143
144 class MovieWebScreen(WebScreen):
145         def __init__(self, session, request):
146                 WebScreen.__init__(self, session, request)
147                 from Components.MovieList import MovieList
148                 from Tools.Directories import resolveFilename,SCOPE_HDD
149                 movielist = MovieList(eServiceReference("2:0:1:0:0:0:0:0:0:0:" + resolveFilename(SCOPE_HDD)))
150                 self["MovieList"] = Movie(session,movielist,func = Movie.LIST)
151                 self["MovieFileDel"] = Movie(session,movielist,func = Movie.DEL)
152                 self["MovieTags"] = Movie(session,movielist,func = Movie.TAGS)
153
154 class MediaPlayerWebScreen(WebScreen):
155         def __init__(self, session, request):
156                 WebScreen.__init__(self, session, request)
157                 self["FileList"] = MP(session,func = MP.LIST)
158                 self["PlayFile"] = MP(session,func = MP.PLAY)
159                 self["Command"] = MP(session,func = MP.COMMAND)
160                 self["WritePlaylist"] = MP(session,func = MP.WRITEPLAYLIST)
161                 
162 class AutoTimerWebScreen(WebScreen):
163         def __init__(self, session, request):
164                 WebScreen.__init__(self, session, request)
165                 self["AutoTimerList"] = AT(session,func = AT.LIST)
166                 self["AutoTimerWrite"] = AT(session,func = AT.WRITE)
167
168 class FilesWebScreen(WebScreen):
169         def __init__(self, session, request):
170                 WebScreen.__init__(self, session, request)
171                 self["DelFile"] = Files(session,func = Files.DEL)
172                 
173 class TimerWebScreen(WebScreen):
174         def __init__(self, session, request):
175                 WebScreen.__init__(self, session, request)
176                 self["TimerList"] = Timer(session,func = Timer.LIST)
177                 self["TimerAddEventID"] = Timer(session,func = Timer.ADDBYID)
178                 self["TimerAdd"] = Timer(session,func = Timer.ADD)
179                 self["TimerDel"] = Timer(session,func = Timer.DEL)
180                 self["TimerChange"] = Timer(session,func = Timer.CHANGE)
181                 self["TimerListWrite"] = Timer(session,func = Timer.WRITE)
182                 self["TVBrowser"] = Timer(session,func = Timer.TVBROWSER)
183                 self["RecordNow"] = Timer(session,func = Timer.RECNOW)
184
185 class RemoteWebScreen(WebScreen):
186         def __init__(self, session, request):
187                 WebScreen.__init__(self, session, request)
188                 self["RemoteControl"] = RemoteControl(session)
189
190 class PowerWebScreen(WebScreen):
191         def __init__(self, session, request):
192                 WebScreen.__init__(self, session, request)
193                 self["PowerState"] = PowerState(session)
194
195 class ParentControlWebScreen(WebScreen):
196         def __init__(self, session, request):
197                 WebScreen.__init__(self, session, request)
198                 self["ParentControlList"] = ParentControl(session)
199                                 
200 class WAPWebScreen(WebScreen):
201         def __init__(self, session, request):
202                 WebScreen.__init__(self, session, request)
203                 self["WAPFillOptionListSyear"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
204                 self["WAPFillOptionListSday"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
205                 self["WAPFillOptionListSmonth"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
206                 self["WAPFillOptionListShour"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
207                 self["WAPFillOptionListSmin"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
208                 
209                 self["WAPFillOptionListEyear"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
210                 self["WAPFillOptionListEday"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
211                 self["WAPFillOptionListEmonth"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
212                 self["WAPFillOptionListEhour"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
213                 self["WAPFillOptionListEmin"] = WAPfunctions(session,func = WAPfunctions.LISTTIME)
214                 
215                 self["WAPFillOptionListRecord"] = WAPfunctions(session,func = WAPfunctions.OPTIONLIST)
216                 self["WAPFillOptionListAfterEvent"] = WAPfunctions(session,func = WAPfunctions.OPTIONLIST)
217                 
218                 self["WAPFillValueName"] = WAPfunctions(session,func = WAPfunctions.FILLVALUE)
219                 self["WAPFillValueDescr"] = WAPfunctions(session,func = WAPfunctions.FILLVALUE)
220
221                 self["WAPFillOptionListRepeated"] = WAPfunctions(session,func = WAPfunctions.REPEATED)
222                 self["WAPServiceList"] = WAPfunctions(session, func = WAPfunctions.SERVICELIST)
223
224                 self["WAPdeleteOldOnSave"] = WAPfunctions(session,func = WAPfunctions.DELETEOLD)
225         
226 class StreamingWebScreen(WebScreen):
227         def __init__(self, session, request):
228                 WebScreen.__init__(self, session, request)
229                 from Components.Sources.StreamService import StreamService
230                 self["StreamService"] = StreamService(self.session.nav)
231
232 class M3UStreamingWebScreen(WebScreen):
233         def __init__(self, session, request):
234                 WebScreen.__init__(self, session, request)
235                 from Components.Sources.StaticText import StaticText
236                 from Components.Sources.Config import Config
237                 from Components.config import config
238                 self["ref"] = StaticText()
239                 self["localip"] = RequestData(request,what=RequestData.HOST)
240
241 class TsM3U(WebScreen):
242         def __init__(self, session, request):
243                 WebScreen.__init__(self, session, request)
244                 from Components.Sources.StaticText import StaticText
245                 from Components.Sources.Config import Config
246                 from Components.config import config
247                 self["file"] = StaticText()
248                 self["localip"] = RequestData(request,what=RequestData.HOST)
249
250 class RestartWebScreen(WebScreen):
251         def __init__(self, session, request):
252                 WebScreen.__init__(self, session, request)
253                 import plugin
254                 plugin.restartWebserver(session)
255                 
256 class GetPid(WebScreen):
257           def __init__(self, session, request):
258                  WebScreen.__init__(self, session, request)
259                  from Components.Sources.StaticText import StaticText
260                  from enigma import iServiceInformation
261                  pids = self.session.nav.getCurrentService()
262                  if pids is not None:
263                         pidinfo = pids.info()
264                         VPID = hex(pidinfo.getInfo(iServiceInformation.sVideoPID))
265                         APID = hex(pidinfo.getInfo(iServiceInformation.sAudioPID))
266                         PPID = hex(pidinfo.getInfo(iServiceInformation.sPMTPID))
267                         self["pids"] = StaticText("%s,%s,%s"%(PPID.lstrip("0x"),VPID.lstrip("0x"),APID.lstrip("0x")))
268                  else:
269                         self["pids"] = StaticText("0x,0x,0x")
270
271                  self["localip"] = RequestData(request,what=RequestData.HOST)
272
273
274 # implements the 'render'-call.
275 # this will act as a downstream_element, like a renderer.
276 class OneTimeElement(Element):
277         def __init__(self, id):
278                 Element.__init__(self)
279                 self.source_id = id
280
281         # CHECKME: is this ok performance-wise?
282         def handleCommand(self, args):
283                 if self.source_id.find(",") >=0:
284                         paramlist = self.source_id.split(",")
285                         list={}
286                         for key in paramlist:
287                                 arg = args.get(key, [])
288                                 if len(arg) == 0:
289                                         list[key] = None        
290                                 elif len(arg) == 1:
291                                         list[key] = "".join(arg)        
292                                 elif len(arg) == 2:
293                                         list[key] = arg[0]
294                         self.source.handleCommand(list)
295                 else:
296                         for c in args.get(self.source_id, []):
297                                 self.source.handleCommand(c)
298
299         def render(self, stream):
300                 t = self.source.getHTML(self.source_id)
301                 stream.write(t)
302
303         def execBegin(self):
304                 self.suspended = False
305
306         def execEnd(self):
307                 self.suspended = True
308
309         def onShow(self):
310                 pass
311
312         def onHide(self):
313                 pass
314
315         def destroy(self):
316                 pass
317
318 class MacroElement(OneTimeElement):
319         def __init__(self, id, macro_dict, macro_name):
320                 OneTimeElement.__init__(self, id)
321                 self.macro_dict = macro_dict
322                 self.macro_name = macro_name
323
324         def render(self, stream):
325                 self.macro_dict[self.macro_name] = self.source.getHTML(self.source_id)
326
327 class StreamingElement(OneTimeElement):
328         def __init__(self, id):
329                 OneTimeElement.__init__(self, id)
330                 self.stream = None
331
332         def changed(self, what):
333                 if self.stream:
334                         self.render(self.stream)
335
336         def setStream(self, stream):
337                 self.stream = stream
338
339 # a to-be-filled list item
340 class ListItem:
341         def __init__(self, name, filternum):
342                 self.name = name
343                 self.filternum = filternum
344
345 class ListMacroItem:
346         def __init__(self, macrodict, macroname):
347                 self.macrodict = macrodict
348                 self.macroname = macroname
349
350 class TextToHTML(Converter):
351         def __init__(self, arg):
352                 Converter.__init__(self, arg)
353
354         def getHTML(self, id):
355                 return self.source.text # encode & etc. here!
356
357 class TextToXML(Converter):
358         def __init__(self, arg):
359                 Converter.__init__(self, arg)
360         
361         def getHTML(self, id):
362                 return escape_xml(self.source.text).replace("\x19", "").replace("\x1c", "").replace("\x1e", "")
363
364 class TextToURL(Converter):
365         def __init__(self, arg):
366                 Converter.__init__(self, arg)
367
368         def getHTML(self, id):
369                 return self.source.text.replace(" ","%20")
370
371 class ReturnEmptyXML(Converter):
372         def __init__(self, arg):
373                 Converter.__init__(self, arg)
374
375         def getHTML(self, id):
376                 return "<rootElement></rootElement>"
377
378 # a null-output. Useful if you only want to issue a command.
379 class Null(Converter):
380         def __init__(self, arg):
381                 Converter.__init__(self, arg)
382
383         def getHTML(self, id):
384                 return ""
385
386 class JavascriptUpdate(Converter):
387         def __init__(self, arg):
388                 Converter.__init__(self, arg)
389
390         def getHTML(self, id):
391                 # 3c5x9, added parent. , this is because the ie loads this in a iframe. an the set is in index.html.xml
392                 #                all other will replace this in JS
393                 return '<script>parent.set("%s", "%s");</script>\n'%(id, self.source.text.replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"').replace('\xb0', '&deg;'))
394
395 # the performant 'listfiller'-engine (plfe)
396 class ListFiller(Converter):
397         def __init__(self, arg):
398                 Converter.__init__(self, arg)
399 #               print "ListFiller-arg: ",arg
400
401         def getText(self):
402                 l = self.source.list
403                 lut = self.source.lut
404                 conv_args = self.converter_arguments
405
406                 # now build a ["string", 1, "string", 2]-styled list, with indices into the
407                 # list to avoid lookup of item name for each entry
408                 lutlist = [ ]
409                 for element in conv_args:
410                         if isinstance(element, basestring):
411                                 lutlist.append((element, None))
412                         elif isinstance(element, ListItem):
413                                 lutlist.append((lut[element.name], element.filternum))
414                         elif isinstance(element, ListMacroItem):
415                                 lutlist.append((element.macrodict[element.macroname], None))
416                         else:
417                                 raise "neither string, ListItem nor ListMacroItem"
418
419                 # now, for the huge list, do:
420                 strlist = [ ]
421                 append = strlist.append
422                 for item in l:
423                         for (element, filternum) in lutlist:
424                                 if not filternum:
425                                         append(element)
426                                 elif filternum == 2:
427                                         append(str(item[element]).replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"'))
428                                 elif filternum == 3:                                    
429                                         append(escape_xml(str(item[element])))
430                                 elif filternum == 4:
431                                         append(str(item[element]).replace("%", "%25").replace("+", "%2B").replace('&', '%26').replace('?', '%3f').replace(' ', '+'))
432                                 else:
433                                         append(str(item[element]))
434                 # (this will be done in c++ later!)
435
436                 return ''.join(strlist)
437
438         text = property(getText)
439
440 class webifHandler(ContentHandler):
441         def __init__(self, session, request):
442                 self.res = [ ]
443                 self.mode = 0
444                 self.screen = None
445                 self.session = session
446                 self.screens = [ ]
447                 self.request = request
448                 self.macros = { }
449
450         def start_element(self, attrs):
451                 scr = self.screen
452
453                 wsource = attrs["source"]
454
455                 path = wsource.split('.')
456                 while len(path) > 1:
457                         scr = self.screen.getRelatedScreen(path[0])
458                         if scr is None:
459                                 print "[webif.py] Parent Screen not found!"
460                                 print wsource
461                         path = path[1:]
462
463                 source = scr.get(path[0])
464
465                 if isinstance(source, ObsoleteSource):
466                         # however, if we found an "obsolete source", issue warning, and resolve the real source.
467                         print "WARNING: WEBIF '%s' USES OBSOLETE SOURCE '%s', USE '%s' INSTEAD!" % (name, wsource, source.new_source)
468                         print "OBSOLETE SOURCE WILL BE REMOVED %s, PLEASE UPDATE!" % (source.removal_date)
469                         if source.description:
470                                 print source.description
471
472                         wsource = source.new_source
473                 else:
474                         pass
475                         # otherwise, use that source.
476
477                 self.source = source
478                 self.source_id = str(attrs.get("id", wsource))
479                 self.is_streaming = "streaming" in attrs
480                 self.macro_name = attrs.get("macro") or None
481
482         def end_element(self):
483                 # instatiate either a StreamingElement or a OneTimeElement, depending on what's required.
484                 if not self.is_streaming:
485                         if self.macro_name is None:
486                                 c = OneTimeElement(self.source_id)
487                         else:
488                                 c = MacroElement(self.source_id, self.macros, self.macro_name)
489                 else:
490                         assert self.macro_name is None
491                         c = StreamingElement(self.source_id)
492
493                 c.connect(self.source)
494                 self.res.append(c)
495                 self.screen.renderer.append(c)
496                 del self.source
497
498         def start_convert(self, attrs):
499                 ctype = attrs["type"]
500
501                         # TODO: we need something better here
502                 if ctype[:4] == "web:": # for now
503                         self.converter = eval(ctype[4:])
504                 else:
505                         try:
506                                 self.converter = my_import('.'.join(["Components", "Converter", ctype])).__dict__.get(ctype)
507                         except ImportError:
508                                 self.converter = my_import('.'.join(["Plugins", "Extensions", "WebInterface", "WebComponents", "Converter", ctype])).__dict__.get(ctype)
509                 self.sub = [ ]
510
511         def end_convert(self):
512                 if len(self.sub) == 1:
513                         self.sub = self.sub[0]
514                 c = self.converter(self.sub)
515                 c.connect(self.source)
516                 self.source = c
517                 del self.sub
518
519         def parse_item(self, attrs):
520                 if "name" in attrs:
521                         filter = {"": 1, "javascript_escape": 2, "xml": 3, "uri": 4}[attrs.get("filter", "")]
522                         self.sub.append(ListItem(attrs["name"], filter))
523                 else:
524                         assert "macro" in attrs, "e2:item must have a name= or macro= attribute!"
525                         self.sub.append(ListMacroItem(self.macros, attrs["macro"]))
526
527         def startElement(self, name, attrs):
528                 if name == "e2:screen":
529                         self.screen = eval(attrs["name"])(self.session,self.request) # fixme
530                         self.screens.append(self.screen)
531                         return
532
533                 if name[:3] == "e2:":
534                         self.mode += 1
535
536                 tag = [' %s="%s"' %(key,val) for (key, val) in attrs.items()]
537                 tag.insert(0, name)
538                 tag.insert(0, '<')
539                 tag.append('>')
540                 tag = ''.join(tag)#.encode('utf-8')
541                 
542                 if self.mode == 0:
543                         self.res.append(tag)
544                 elif self.mode == 1: # expect "<e2:element>"
545                         assert name == "e2:element", "found %s instead of e2:element" % name
546                         self.start_element(attrs)
547                 elif self.mode == 2: # expect "<e2:convert>"
548                         if name[:3] == "e2:":
549                                 assert name == "e2:convert"
550                                 self.start_convert(attrs)
551                         else:
552                                 self.sub.append(tag)
553                 elif self.mode == 3:
554                         assert name == "e2:item", "found %s instead of e2:item!" % name
555                         
556                         self.parse_item(attrs)
557
558         def endElement(self, name):
559                 if name == "e2:screen":
560                         self.screen = None
561                         return
562
563                 tag = "</" + name + ">"
564                 if self.mode == 0:
565                         self.res.append(tag)
566                 elif self.mode == 2 and name[:3] != "e2:":
567                         self.sub.append(tag)
568                 elif self.mode == 2: # closed 'convert' -> sub
569                         self.end_convert()
570                 elif self.mode == 1: # closed 'element'
571                         self.end_element()
572                 if name[:3] == "e2:":
573                         self.mode -= 1
574
575         def processingInstruction(self, target, data):
576                 self.res.append('<?' + target + ' ' + data + '>')
577
578         def characters(self, ch):
579                 ch = ch.encode('utf-8')
580                 if self.mode == 0:
581                         self.res.append(ch)
582                 elif self.mode == 2:
583                         self.sub.append(ch)
584
585         def startEntity(self, name):
586                 self.res.append('&' + name + ';');
587
588         def execBegin(self):
589                 for screen in self.screens:
590                         screen.execBegin()
591
592         def cleanup(self):
593                 print "screen cleanup!"
594                 for screen in self.screens:
595                         screen.execEnd()
596                         screen.doClose()
597                 self.screens = [ ]
598
599 def renderPage(stream, path, req, session):
600         
601         # read in the template, create required screens
602         # we don't have persistense yet.
603         # if we had, this first part would only be done once.
604         handler = webifHandler(session,req)
605         parser = make_parser()
606         parser.setFeature(feature_namespaces, 0)
607         parser.setContentHandler(handler)
608         parser.parse(open(util.sibpath(__file__, path)))
609         
610         # by default, we have non-streaming pages
611         finish = True
612         
613         # first, apply "commands" (aka. URL argument)
614         for x in handler.res:
615                 if isinstance(x, Element):
616                         x.handleCommand(req.args)
617
618         handler.execBegin()
619
620         # now, we have a list with static texts mixed
621         # with non-static Elements.
622         # flatten this list, write into the stream.
623         for x in handler.res:
624                 if isinstance(x, Element):
625                         if isinstance(x, StreamingElement):
626                                 finish = False
627                                 x.setStream(stream)
628                         x.render(stream)
629                 else:
630                         stream.write(str(x))
631
632         def ping(s):
633                 from twisted.internet import reactor
634                 s.write("\n");
635                 reactor.callLater(3, ping, s)
636         
637         # if we met a "StreamingElement", there is at least one
638         # element which wants to output data more than once,
639         # i.e. on host-originated changes.
640         # in this case, don't finish yet, don't cleanup yet,
641         # but instead do that when the client disconnects.
642         if finish:
643                 streamFinish(handler, stream)
644         else:
645                 # ok.
646                 # you *need* something which constantly sends something in a regular interval,
647                 # in order to detect disconnected clients.
648                 # i agree that this "ping" sucks terrible, so better be sure to have something 
649                 # similar. A "CurrentTime" is fine. Or anything that creates *some* output.
650                 ping(stream)
651                 stream.closed_callback = lambda : streamFinish(handler, stream)
652
653 def streamFinish(handler, stream):
654         handler.cleanup()
655         stream.finish()
656         del handler
657         del stream