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