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