Merge new Webinterface to HEAD
[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()
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          self["localip"] = RequestData(request,what=RequestData.HOST)
269
270
271 # implements the 'render'-call.
272 # this will act as a downstream_element, like a renderer.
273 class OneTimeElement(Element):
274         def __init__(self, id):
275                 Element.__init__(self)
276                 self.source_id = id
277
278         # CHECKME: is this ok performance-wise?
279         def handleCommand(self, args):
280                 if self.source_id.find(",") >=0:
281                         paramlist = self.source_id.split(",")
282                         list={}
283                         for key in paramlist:
284                                 arg = args.get(key, [])
285                                 if len(arg) == 0:
286                                         list[key] = None        
287                                 elif len(arg) == 1:
288                                         list[key] = "".join(arg)        
289                                 elif len(arg) == 2:
290                                         list[key] = arg[0]
291                         self.source.handleCommand(list)
292                 else:
293                         for c in args.get(self.source_id, []):
294                                 self.source.handleCommand(c)
295
296         def render(self, stream):
297                 t = self.source.getHTML(self.source_id)
298                 stream.write(t)
299
300         def execBegin(self):
301                 pass
302
303         def execEnd(self):
304                 pass
305
306         def onShow(self):
307                 pass
308
309         def onHide(self):
310                 pass
311
312         def destroy(self):
313                 pass
314
315 class MacroElement(OneTimeElement):
316         def __init__(self, id, macro_dict, macro_name):
317                 OneTimeElement.__init__(self, id)
318                 self.macro_dict = macro_dict
319                 self.macro_name = macro_name
320
321         def render(self, stream):
322                 self.macro_dict[self.macro_name] = self.source.getHTML(self.source_id)
323
324 class StreamingElement(OneTimeElement):
325         def __init__(self, id):
326                 OneTimeElement.__init__(self, id)
327                 self.stream = None
328
329         def changed(self, what):
330                 if self.stream:
331                         self.render(self.stream)
332
333         def setStream(self, stream):
334                 self.stream = stream
335
336 # a to-be-filled list item
337 class ListItem:
338         def __init__(self, name, filternum):
339                 self.name = name
340                 self.filternum = filternum
341
342 class ListMacroItem:
343         def __init__(self, macrodict, macroname):
344                 self.macrodict = macrodict
345                 self.macroname = macroname
346
347 class TextToHTML(Converter):
348         def __init__(self, arg):
349                 Converter.__init__(self, arg)
350
351         def getHTML(self, id):
352                 return self.source.text # encode & etc. here!
353
354 class TextToXML(Converter):
355         def __init__(self, arg):
356                 Converter.__init__(self, arg)
357         
358         def getHTML(self, id):
359                 return escape_xml(self.source.text).replace("\x19", "").replace("\x1c", "").replace("\x1e", "")
360
361 class TextToURL(Converter):
362         def __init__(self, arg):
363                 Converter.__init__(self, arg)
364
365         def getHTML(self, id):
366                 return self.source.text.replace(" ","%20")
367
368 class ReturnEmptyXML(Converter):
369         def __init__(self, arg):
370                 Converter.__init__(self, arg)
371
372         def getHTML(self, id):
373                 return "<rootElement></rootElement>"
374
375 # a null-output. Useful if you only want to issue a command.
376 class Null(Converter):
377         def __init__(self, arg):
378                 Converter.__init__(self, arg)
379
380         def getHTML(self, id):
381                 return ""
382
383 class JavascriptUpdate(Converter):
384         def __init__(self, arg):
385                 Converter.__init__(self, arg)
386
387         def getHTML(self, id):
388                 # 3c5x9, added parent. , this is because the ie loads this in a iframe. an the set is in index.html.xml
389                 #                all other will replace this in JS
390                 return '<script>parent.set("%s", "%s");</script>\n'%(id, self.source.text.replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"').replace('\xb0', '&deg;'))
391
392 # the performant 'listfiller'-engine (plfe)
393 class ListFiller(Converter):
394         def __init__(self, arg):
395                 Converter.__init__(self, arg)
396 #               print "ListFiller-arg: ",arg
397
398         def getText(self):
399                 l = self.source.list
400                 lut = self.source.lut
401                 conv_args = self.converter_arguments
402
403                 # now build a ["string", 1, "string", 2]-styled list, with indices into the
404                 # list to avoid lookup of item name for each entry
405                 lutlist = [ ]
406                 for element in conv_args:
407                         if isinstance(element, basestring):
408                                 lutlist.append((element, None))
409                         elif isinstance(element, ListItem):
410                                 lutlist.append((lut[element.name], element.filternum))
411                         elif isinstance(element, ListMacroItem):
412                                 lutlist.append((element.macrodict[element.macroname], None))
413                         else:
414                                 raise "neither string, ListItem nor ListMacroItem"
415
416                 # now, for the huge list, do:
417                 strlist = [ ]
418                 append = strlist.append
419                 for item in l:
420                         for (element, filternum) in lutlist:
421                                 if not filternum:
422                                         append(element)
423                                 elif filternum == 2:
424                                         append(str(item[element]).replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"'))
425                                 elif filternum == 3:
426                                         #append(str(item[element]).replace("&", "&amp;").replace("<", "&lt;").replace('"', '&quot;').replace(">", "&gt;"))
427                                         append(escape_xml(str(item[element])))
428                                 elif filternum == 4:
429                                         append(str(item[element]).replace("%", "%25").replace("+", "%2B").replace('&', '%26').replace('?', '%3f').replace(' ', '+'))
430                                 else:
431                                         append(str(item[element]))
432                 # (this will be done in c++ later!)
433 #               print l
434 #               print lut
435                 return ''.join(strlist)
436
437         text = property(getText)
438
439 class webifHandler(ContentHandler):
440         def __init__(self, session, request):
441                 self.res = [ ]
442                 self.mode = 0
443                 self.screen = None
444                 self.session = session
445                 self.screens = [ ]
446                 self.request = request
447                 self.macros = { }
448
449         def start_element(self, attrs):
450                 scr = self.screen
451
452                 wsource = attrs["source"]
453
454                 path = wsource.split('.')
455                 while len(path) > 1:
456                         scr = self.screen.getRelatedScreen(path[0])
457                         if scr is None:
458                                 print "[webif.py] Parent Screen not found!"
459                                 print wsource
460                         path = path[1:]
461
462                 source = scr.get(path[0])
463
464                 if isinstance(source, ObsoleteSource):
465                         # however, if we found an "obsolete source", issue warning, and resolve the real source.
466                         print "WARNING: WEBIF '%s' USES OBSOLETE SOURCE '%s', USE '%s' INSTEAD!" % (name, wsource, source.new_source)
467                         print "OBSOLETE SOURCE WILL BE REMOVED %s, PLEASE UPDATE!" % (source.removal_date)
468                         if source.description:
469                                 print source.description
470
471                         wsource = source.new_source
472                 else:
473                         pass
474                         # otherwise, use that source.
475
476                 self.source = source
477                 self.source_id = str(attrs.get("id", wsource))
478                 self.is_streaming = "streaming" in attrs
479                 self.macro_name = attrs.get("macro") or None
480
481         def end_element(self):
482                 # instatiate either a StreamingElement or a OneTimeElement, depending on what's required.
483                 if not self.is_streaming:
484                         if self.macro_name is None:
485                                 c = OneTimeElement(self.source_id)
486                         else:
487                                 c = MacroElement(self.source_id, self.macros, self.macro_name)
488                 else:
489                         assert self.macro_name is None
490                         c = StreamingElement(self.source_id)
491
492                 c.connect(self.source)
493                 self.res.append(c)
494                 self.screen.renderer.append(c)
495                 del self.source
496
497         def start_convert(self, attrs):
498                 ctype = attrs["type"]
499
500                         # TODO: we need something better here
501                 if ctype[:4] == "web:": # for now
502                         self.converter = eval(ctype[4:])
503                 else:
504                         try:
505                                 self.converter = my_import('.'.join(["Components", "Converter", ctype])).__dict__.get(ctype)
506                         except ImportError:
507                                 self.converter = my_import('.'.join(["Plugins", "Extensions", "WebInterface", "WebComponents", "Converter", ctype])).__dict__.get(ctype)
508                 self.sub = [ ]
509
510         def end_convert(self):
511                 if len(self.sub) == 1:
512                         self.sub = self.sub[0]
513                 c = self.converter(self.sub)
514                 c.connect(self.source)
515                 self.source = c
516                 del self.sub
517
518         def parse_item(self, attrs):
519                 if "name" in attrs:
520                         filter = {"": 1, "javascript_escape": 2, "xml": 3, "uri": 4}[attrs.get("filter", "")]
521                         self.sub.append(ListItem(attrs["name"], filter))
522                 else:
523                         assert "macro" in attrs, "e2:item must have a name= or macro= attribute!"
524                         self.sub.append(ListMacroItem(self.macros, attrs["macro"]))
525
526         def startElement(self, name, attrs):
527                 if name == "e2:screen":
528                         self.screen = eval(attrs["name"])(self.session,self.request) # fixme
529                         self.screens.append(self.screen)
530                         return
531
532                 if name[:3] == "e2:":
533                         self.mode += 1
534
535                 tag = [' %s="%s"' %(key,val) for (key, val) in attrs.items()]
536                 tag.insert(0, name)
537                 tag.insert(0, '<')
538                 tag.append('>')
539                 tag = ''.join(tag)#.encode('utf-8')
540                 
541                 if self.mode == 0:
542                         self.res.append(tag)
543                 elif self.mode == 1: # expect "<e2:element>"
544                         assert name == "e2:element", "found %s instead of e2:element" % name
545                         self.start_element(attrs)
546                 elif self.mode == 2: # expect "<e2:convert>"
547                         if name[:3] == "e2:":
548                                 assert name == "e2:convert"
549                                 self.start_convert(attrs)
550                         else:
551                                 self.sub.append(tag)
552                 elif self.mode == 3:
553                         assert name == "e2:item", "found %s instead of e2:item!" % name
554                         
555                         self.parse_item(attrs)
556
557         def endElement(self, name):
558                 if name == "e2:screen":
559                         self.screen = None
560                         return
561
562                 tag = "</" + name + ">"
563                 if self.mode == 0:
564                         self.res.append(tag)
565                 elif self.mode == 2 and name[:3] != "e2:":
566                         self.sub.append(tag)
567                 elif self.mode == 2: # closed 'convert' -> sub
568                         self.end_convert()
569                 elif self.mode == 1: # closed 'element'
570                         self.end_element()
571                 if name[:3] == "e2:":
572                         self.mode -= 1
573
574         def processingInstruction(self, target, data):
575                 self.res.append('<?' + target + ' ' + data + '>')
576
577         def characters(self, ch):
578                 ch = ch.encode('utf-8')
579                 if self.mode == 0:
580                         self.res.append(ch)
581                 elif self.mode == 2:
582                         self.sub.append(ch)
583
584         def startEntity(self, name):
585                 self.res.append('&' + name + ';');
586
587         def execBegin(self):
588                 for screen in self.screens:
589                         screen.execBegin()
590
591         def cleanup(self):
592                 print "screen cleanup!"
593                 for screen in self.screens:
594                         screen.execEnd()
595                         screen.doClose()
596                 self.screens = [ ]
597
598 def renderPage(stream, path, req, session):
599         
600         # read in the template, create required screens
601         # we don't have persistense yet.
602         # if we had, this first part would only be done once.
603         handler = webifHandler(session,req)
604         parser = make_parser()
605         parser.setFeature(feature_namespaces, 0)
606         parser.setContentHandler(handler)
607         parser.parse(open(util.sibpath(__file__, path)))
608         
609         # by default, we have non-streaming pages
610         finish = True
611         
612         # first, apply "commands" (aka. URL argument)
613         for x in handler.res:
614                 if isinstance(x, Element):
615                         x.handleCommand(req.args)
616
617         handler.execBegin()
618
619         # now, we have a list with static texts mixed
620         # with non-static Elements.
621         # flatten this list, write into the stream.
622         for x in handler.res:
623                 if isinstance(x, Element):
624                         if isinstance(x, StreamingElement):
625                                 finish = False
626                                 x.setStream(stream)
627                         x.render(stream)
628                 else:
629                         stream.write(str(x))
630
631         def ping(s):
632                 from twisted.internet import reactor
633                 s.write("\n");
634                 reactor.callLater(3, ping, s)
635         
636         # if we met a "StreamingElement", there is at least one
637         # element which wants to output data more than once,
638         # i.e. on host-originated changes.
639         # in this case, don't finish yet, don't cleanup yet,
640         # but instead do that when the client disconnects.
641         if finish:
642                 streamFinish(handler, stream)
643         else:
644                 # ok.
645                 # you *need* something which constantly sends something in a regular interval,
646                 # in order to detect disconnected clients.
647                 # i agree that this "ping" sucks terrible, so better be sure to have something 
648                 # similar. A "CurrentTime" is fine. Or anything that creates *some* output.
649                 ping(stream)
650                 stream.closed_callback = lambda : streamFinish(handler, stream)
651
652 def streamFinish(handler, stream):
653         handler.cleanup()
654         stream.finish()
655         del handler
656         del stream