1# -*- test-case-name: nevow.test.test_athena -*- 2 3import itertools, os, re, warnings, StringIO 4 5from zope.interface import implements 6 7from twisted.internet import defer, error, reactor 8from twisted.python import log, failure, context 9from twisted.python.util import sibpath 10from twisted import plugin 11 12from nevow import inevow, plugins, flat, _flat 13from nevow import rend, loaders, static 14from nevow import json, util, tags, guard, stan 15from nevow.util import CachedFile 16from nevow.useragent import UserAgent, browsers 17from nevow.url import here, URL 18 19from nevow.page import Element, renderer 20 21ATHENA_XMLNS_URI = "http://divmod.org/ns/athena/0.7" 22ATHENA_RECONNECT = "__athena_reconnect__" 23 24expose = util.Expose( 25 """ 26 Allow one or more methods to be invoked by the client:: 27 28 | class Foo(LiveElement): 29 | def twiddle(self, x, y): 30 | ... 31 | def frob(self, a, b): 32 | ... 33 | expose(twiddle, frob) 34 35 The Widget for Foo will be allowed to invoke C{twiddle} and C{frob}. 36 """) 37 38 39 40class OrphanedFragment(Exception): 41 """ 42 Raised when an operation requiring a parent is attempted on an unattached 43 child. 44 """ 45 46 47 48class LivePageError(Exception): 49 """ 50 Base exception for LivePage errors. 51 """ 52 jsClass = u'Divmod.Error' 53 54 55 56class NoSuchMethod(LivePageError): 57 """ 58 Raised when an attempt is made to invoke a method which is not defined or 59 exposed. 60 """ 61 jsClass = u'Nevow.Athena.NoSuchMethod' 62 63 def __init__(self, objectID, methodName): 64 self.objectID = objectID 65 self.methodName = methodName 66 LivePageError.__init__(self, objectID, methodName) 67 68 69 70def neverEverCache(request): 71 """ 72 Set headers to indicate that the response to this request should never, 73 ever be cached. 74 """ 75 request.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate') 76 request.setHeader('Pragma', 'no-cache') 77 78 79def activeChannel(request): 80 """ 81 Mark this connection as a 'live' channel by setting the Connection: close 82 header and flushing all headers immediately. 83 """ 84 request.setHeader("Connection", "close") 85 request.write('') 86 87 88 89class MappingResource(object): 90 """ 91 L{inevow.IResource} which looks up segments in a mapping between symbolic 92 names and the files they correspond to. 93 94 @type mapping: C{dict} 95 @ivar mapping: A map between symbolic, requestable names (eg, 96 'Nevow.Athena') and C{str} instances which name files containing data 97 which should be served in response. 98 """ 99 implements(inevow.IResource) 100 101 def __init__(self, mapping): 102 self.mapping = mapping 103 104 105 def renderHTTP(self, ctx): 106 return rend.FourOhFour() 107 108 109 def resourceFactory(self, fileName): 110 """ 111 Retrieve an L{inevow.IResource} which will render the contents of 112 C{fileName}. 113 """ 114 return static.File(fileName) 115 116 117 def locateChild(self, ctx, segments): 118 try: 119 impl = self.mapping[segments[0]] 120 except KeyError: 121 return rend.NotFound 122 else: 123 return self.resourceFactory(impl), [] 124 125 126 127def _dependencyOrdered(coll, memo): 128 """ 129 @type coll: iterable of modules 130 @param coll: The initial sequence of modules. 131 132 @type memo: C{dict} 133 @param memo: A dictionary mapping module names to their dependencies that 134 will be used as a mutable cache. 135 """ 136 137 138 139class AthenaModule(object): 140 """ 141 A representation of a chunk of stuff in a file which can depend on other 142 chunks of stuff in other files. 143 """ 144 _modules = {} 145 146 lastModified = 0 147 deps = None 148 packageDeps = [] 149 150 def getOrCreate(cls, name, mapping): 151 # XXX This implementation of getOrCreate precludes the 152 # simultaneous co-existence of several different package 153 # namespaces. 154 if name in cls._modules: 155 return cls._modules[name] 156 mod = cls._modules[name] = cls(name, mapping) 157 return mod 158 getOrCreate = classmethod(getOrCreate) 159 160 161 def __init__(self, name, mapping): 162 self.name = name 163 self.mapping = mapping 164 165 if '.' in name: 166 parent = '.'.join(name.split('.')[:-1]) 167 self.packageDeps = [self.getOrCreate(parent, mapping)] 168 169 self._cache = CachedFile(self.mapping[self.name], self._getDeps) 170 171 172 def __repr__(self): 173 return '%s(%r)' % (self.__class__.__name__, self.name,) 174 175 176 _importExpression = re.compile('^// import (.+)$', re.MULTILINE) 177 def _extractImports(self, fileObj): 178 s = fileObj.read() 179 for m in self._importExpression.finditer(s): 180 yield self.getOrCreate(m.group(1).decode('ascii'), self.mapping) 181 182 183 184 def _getDeps(self, jsFile): 185 """ 186 Calculate our dependencies given the path to our source. 187 """ 188 depgen = self._extractImports(file(jsFile, 'rU')) 189 return self.packageDeps + dict.fromkeys(depgen).keys() 190 191 192 def dependencies(self): 193 """ 194 Return a list of names of other JavaScript modules we depend on. 195 """ 196 return self._cache.load() 197 198 199 def allDependencies(self, memo=None): 200 """ 201 Return the transitive closure of dependencies, including this module. 202 203 The transitive dependencies for this module will be ordered such that 204 any particular module is located after all of its dependencies, with no 205 module occurring more than once. 206 207 The dictionary passed in for C{memo} will be modified in-place; if it 208 is reused across multiple calls, dependencies calculated during a 209 previous invocation will not be recalculated again. 210 211 @type memo: C{dict} of C{str: list of AthenaModule} 212 @param memo: A dictionary mapping module names to the modules they 213 depend on that will be used as a mutable cache. 214 215 @rtype: C{list} of C{AthenaModule} 216 """ 217 if memo is None: 218 memo = {} 219 ordered = [] 220 221 def _getDeps(dependent): 222 if dependent.name in memo: 223 deps = memo[dependent.name] 224 else: 225 memo[dependent.name] = deps = dependent.dependencies() 226 return deps 227 228 def _insertDep(dependent): 229 if dependent not in ordered: 230 for dependency in _getDeps(dependent): 231 _insertDep(dependency) 232 ordered.append(dependent) 233 234 _insertDep(self) 235 return ordered 236 237 238 239class JSModule(AthenaModule): 240 """ 241 L{AthenaModule} subclass for dealing with Javascript modules. 242 """ 243 _modules= {} 244 245 246 247class CSSModule(AthenaModule): 248 """ 249 L{AthenaModule} subclass for dealing with CSS modules. 250 """ 251 _modules = {} 252 253 254 255class JSPackage(object): 256 """ 257 A Javascript package. 258 259 @type mapping: C{dict} 260 @ivar mapping: Mapping between JS module names and C{str} representing 261 filesystem paths containing their implementations. 262 """ 263 implements(plugin.IPlugin, inevow.IJavascriptPackage) 264 265 def __init__(self, mapping): 266 self.mapping = mapping 267 268 269 270def _collectPackageBelow(baseDir, extension): 271 """ 272 Assume a filesystem package hierarchy starting at C{baseDir}. Collect all 273 files within it ending with C{extension} into a mapping between 274 dot-separated symbolic module names and their corresponding filesystem 275 paths. 276 277 Note that module/package names beginning with . are ignored. 278 279 @type baseDir: C{str} 280 @param baseDir: A path to the root of a package hierarchy on a filesystem. 281 282 @type extension: C{str} 283 @param extension: The filename extension we're interested in (e.g. 'css' 284 or 'js'). 285 286 @rtype: C{dict} 287 @return: Mapping between C{unicode} module names and their corresponding 288 C{str} filesystem paths. 289 """ 290 mapping = {} 291 EMPTY = sibpath(__file__, 'empty-module.' + extension) 292 293 _revMap = {baseDir: ''} 294 for (root, dirs, filenames) in os.walk(baseDir): 295 stem = _revMap[root] 296 dirs[:] = [d for d in dirs if not d.startswith('.')] 297 298 for dir in dirs: 299 name = stem + dir 300 path = os.path.join(root, dir, '__init__.' + extension) 301 if not os.path.exists(path): 302 path = EMPTY 303 mapping[unicode(name, 'ascii')] = path 304 _revMap[os.path.join(root, dir)] = name + '.' 305 306 for fn in filenames: 307 if fn.startswith('.'): 308 continue 309 310 if fn == '__init__.' + extension: 311 continue 312 313 if not fn.endswith('.' + extension): 314 continue 315 316 name = stem + fn[:-(len(extension) + 1)] 317 path = os.path.join(root, fn) 318 mapping[unicode(name, 'ascii')] = path 319 return mapping 320 321 322 323class AutoJSPackage(object): 324 """ 325 A L{inevow.IJavascriptPackage} implementation that scans an on-disk 326 hierarchy locating modules and packages. 327 328 @type baseDir: C{str} 329 @ivar baseDir: A path to the root of a JavaScript packages/modules 330 filesystem hierarchy. 331 """ 332 implements(plugin.IPlugin, inevow.IJavascriptPackage) 333 334 def __init__(self, baseDir): 335 self.mapping = _collectPackageBelow(baseDir, 'js') 336 337 338 339class AutoCSSPackage(object): 340 """ 341 Like L{AutoJSPackage}, but for CSS packages. Modules within this package 342 can be referenced by L{LivePage.cssModule} or L{LiveElement.cssModule}. 343 """ 344 implements(plugin.IPlugin, inevow.ICSSPackage) 345 346 def __init__(self, baseDir): 347 self.mapping = _collectPackageBelow(baseDir, 'css') 348 349 350 351def allJavascriptPackages(): 352 """ 353 Return a dictionary mapping JavaScript module names to local filenames 354 which implement those modules. This mapping is constructed from all the 355 C{IJavascriptPackage} plugins available on the system. It also includes 356 C{Nevow.Athena} as a special case. 357 """ 358 d = {} 359 for p in plugin.getPlugIns(inevow.IJavascriptPackage, plugins): 360 d.update(p.mapping) 361 return d 362 363 364 365def allCSSPackages(): 366 """ 367 Like L{allJavascriptPackages}, but for CSS packages. 368 """ 369 d = {} 370 for p in plugin.getPlugIns(inevow.ICSSPackage, plugins): 371 d.update(p.mapping) 372 return d 373 374 375 376class JSDependencies(object): 377 """ 378 Keeps track of which JavaScript files depend on which other 379 JavaScript files (because JavaScript is a very poor language and 380 cannot do this itself). 381 """ 382 _loadPlugins = False 383 384 def __init__(self, mapping=None): 385 if mapping is None: 386 self.mapping = {} 387 self._loadPlugins = True 388 else: 389 self.mapping = mapping 390 391 392 def getModuleForName(self, className): 393 """ 394 Return the L{JSModule} most likely to define the given name. 395 """ 396 if self._loadPlugins: 397 self.mapping.update(allJavascriptPackages()) 398 self._loadPlugins = False 399 400 jsMod = className 401 while jsMod: 402 try: 403 self.mapping[jsMod] 404 except KeyError: 405 if '.' not in jsMod: 406 break 407 jsMod = '.'.join(jsMod.split('.')[:-1]) 408 else: 409 return JSModule.getOrCreate(jsMod, self.mapping) 410 raise RuntimeError("Unknown class: %r" % (className,)) 411 getModuleForClass = getModuleForName 412 413 414jsDeps = JSDependencies() 415 416 417 418class CSSRegistry(object): 419 """ 420 Keeps track of a set of CSS modules. 421 """ 422 def __init__(self, mapping=None): 423 if mapping is None: 424 mapping = {} 425 loadPlugins = True 426 else: 427 loadPlugins = False 428 self.mapping = mapping 429 self._loadPlugins = loadPlugins 430 431 432 def getModuleForName(self, moduleName): 433 """ 434 Turn a CSS module name into an L{AthenaModule}. 435 436 @type moduleName: C{unicode} 437 438 @rtype: L{CSSModule} 439 """ 440 if self._loadPlugins: 441 self.mapping.update(allCSSPackages()) 442 self._loadPlugins = False 443 try: 444 self.mapping[moduleName] 445 except KeyError: 446 raise RuntimeError('Unknown CSS module: %r' % (moduleName,)) 447 return CSSModule.getOrCreate(moduleName, self.mapping) 448 449_theCSSRegistry = CSSRegistry() 450 451 452 453class JSException(Exception): 454 """ 455 Exception class to wrap remote exceptions from JavaScript. 456 """ 457 458 459 460class JSCode(object): 461 """ 462 Class for mock code objects in mock JS frames. 463 """ 464 def __init__(self, name, filename): 465 self.co_name = name 466 self.co_filename = filename 467 468 469 470class JSFrame(object): 471 """ 472 Class for mock frame objects in JS client-side traceback wrappers. 473 """ 474 def __init__(self, func, fname, ln): 475 self.f_back = None 476 self.f_locals = {} 477 self.f_globals = {} 478 self.f_code = JSCode(func, fname) 479 self.f_lineno = ln 480 481 482 483class JSTraceback(object): 484 """ 485 Class for mock traceback objects representing client-side JavaScript 486 tracebacks. 487 """ 488 def __init__(self, frame, ln): 489 self.tb_frame = frame 490 self.tb_lineno = ln 491 self.tb_next = None 492 493 494 495def parseStack(stack): 496 """ 497 Extract function name, file name, and line number information from the 498 string representation of a JavaScript trace-back. 499 """ 500 frames = [] 501 for line in stack.split('\n'): 502 if '@' not in line: 503 continue 504 func, rest = line.split('@', 1) 505 if ':' not in rest: 506 continue 507 508 divide = rest.rfind(':') 509 if divide == -1: 510 fname, ln = rest, '' 511 else: 512 fname, ln = rest[:divide], rest[divide + 1:] 513 ln = int(ln) 514 frames.insert(0, (func, fname, ln)) 515 return frames 516 517 518 519def buildTraceback(frames, modules): 520 """ 521 Build a chain of mock traceback objects from a serialized Error (or other 522 exception) object, and return the head of the chain. 523 """ 524 last = None 525 first = None 526 for func, fname, ln in frames: 527 fname = modules.get(fname.split('/')[-1], fname) 528 frame = JSFrame(func, fname, ln) 529 tb = JSTraceback(frame, ln) 530 if last: 531 last.tb_next = tb 532 else: 533 first = tb 534 last = tb 535 return first 536 537 538 539def getJSFailure(exc, modules): 540 """ 541 Convert a serialized client-side exception to a Failure. 542 """ 543 text = '%s: %s' % (exc[u'name'], exc[u'message']) 544 545 frames = [] 546 if u'stack' in exc: 547 frames = parseStack(exc[u'stack']) 548 549 return failure.Failure(JSException(text), exc_tb=buildTraceback(frames, modules)) 550 551 552 553class LivePageTransport(object): 554 implements(inevow.IResource) 555 556 def __init__(self, messageDeliverer, useActiveChannels=True): 557 self.messageDeliverer = messageDeliverer 558 self.useActiveChannels = useActiveChannels 559 560 561 def locateChild(self, ctx, segments): 562 return rend.NotFound 563 564 565 def renderHTTP(self, ctx): 566 req = inevow.IRequest(ctx) 567 neverEverCache(req) 568 if self.useActiveChannels: 569 activeChannel(req) 570 571 requestContent = req.content.read() 572 messageData = json.parse(requestContent) 573 574 response = self.messageDeliverer.basketCaseReceived(ctx, messageData) 575 response.addCallback(json.serialize) 576 req.notifyFinish().addErrback(lambda err: self.messageDeliverer._unregisterDeferredAsOutputChannel(response)) 577 return response 578 579 580 581class LivePageFactory: 582 noisy = True 583 584 def __init__(self): 585 self.clients = {} 586 587 def addClient(self, client): 588 clientID = self._newClientID() 589 self.clients[clientID] = client 590 if self.noisy: 591 log.msg("Rendered new LivePage %r: %r" % (client, clientID)) 592 return clientID 593 594 def getClient(self, clientID): 595 return self.clients[clientID] 596 597 def removeClient(self, clientID): 598 # State-tracking bugs may make it tempting to make the next line a 599 # 'pop', but it really shouldn't be; if the Page instance with this 600 # client ID is already gone, then it should be gone, which means that 601 # this method can't be called with that argument. 602 del self.clients[clientID] 603 if self.noisy: 604 log.msg("Disconnected old LivePage %r" % (clientID,)) 605 606 def _newClientID(self): 607 return guard._sessionCookie() 608 609 610_thePrivateAthenaResource = static.File(util.resource_filename('nevow', 'athena_private')) 611 612 613class ConnectFailed(Exception): 614 pass 615 616 617class ConnectionLost(Exception): 618 pass 619 620 621CLOSE = u'close' 622UNLOAD = u'unload' 623 624class ReliableMessageDelivery(object): 625 """ 626 A reliable message delivery abstraction over a possibly unreliable transport. 627 628 @type livePage: L{LivePage} 629 @ivar livePage: The page this delivery is associated with. 630 631 @type connectTimeout: C{int} 632 @ivar connectTimeout: The amount of time (in seconds) to wait for the 633 initial connection, before timing out. 634 635 @type transportlessTimeout: C{int} 636 @ivar transportlessTimeout: The amount of time (in seconds) to wait for 637 another transport to connect if none are currently connected, before 638 timing out. 639 640 @type idleTimeout: C{int} 641 @ivar idleTimeout: The maximum amount of time (in seconds) to leave a 642 connected transport, before sending a noop response. 643 644 @type connectionLost: callable or C{None} 645 @ivar connectionLost: A callback invoked with a L{failure.Failure} if the 646 connection with the client is lost (due to a timeout, for example). 647 648 @type scheduler: callable or C{None} 649 @ivar scheduler: If passed, this is used in place of C{reactor.callLater}. 650 651 @type connectionMade: callable or C{None} 652 @ivar connectionMade: A callback invoked with no arguments when it first 653 becomes possible to to send a message to the client. 654 """ 655 _paused = 0 656 _stopped = False 657 _connected = False 658 659 outgoingAck = -1 # sequence number which has been acknowledged 660 # by this end of the connection. 661 662 outgoingSeq = -1 # sequence number of the next message to be 663 # added to the outgoing queue. 664 665 def __init__(self, 666 livePage, 667 connectTimeout=60, transportlessTimeout=30, idleTimeout=300, 668 connectionLost=None, 669 scheduler=None, 670 connectionMade=None): 671 self.livePage = livePage 672 self.messages = [] 673 self.outputs = [] 674 self.connectTimeout = connectTimeout 675 self.transportlessTimeout = transportlessTimeout 676 self.idleTimeout = idleTimeout 677 if scheduler is None: 678 scheduler = reactor.callLater 679 self.scheduler = scheduler 680 self._transportlessTimeoutCall = self.scheduler(self.connectTimeout, self._connectTimedOut) 681 self.connectionMade = connectionMade 682 self.connectionLost = connectionLost 683 684 685 def _connectTimedOut(self): 686 self._transportlessTimeoutCall = None 687 self.connectionLost(failure.Failure(ConnectFailed("Timeout"))) 688 689 690 def _transportlessTimedOut(self): 691 self._transportlessTimeoutCall = None 692 self.connectionLost(failure.Failure(ConnectionLost("Timeout"))) 693 694 695 def _idleTimedOut(self): 696 output, timeout = self.outputs.pop(0) 697 if not self.outputs: 698 self._transportlessTimeoutCall = self.scheduler(self.transportlessTimeout, self._transportlessTimedOut) 699 output([self.outgoingAck, []]) 700 701 702 def _sendMessagesToOutput(self, output): 703 log.msg(athena_send_messages=True, count=len(self.messages)) 704 output([self.outgoingAck, self.messages]) 705 706 707 def pause(self): 708 self._paused += 1 709 710 711 def _trySendMessages(self): 712 """ 713 If we have pending messages and there is an available transport, then 714 consume it to send the messages. 715 """ 716 if self.messages and self.outputs: 717 output, timeout = self.outputs.pop(0) 718 timeout.cancel() 719 if not self.outputs: 720 self._transportlessTimeoutCall = self.scheduler(self.transportlessTimeout, self._transportlessTimedOut) 721 self._sendMessagesToOutput(output) 722 723 724 def unpause(self): 725 """ 726 Decrement the pause counter and if the resulting state is not still 727 paused try to flush any pending messages and expend excess outputs. 728 """ 729 self._paused -= 1 730 if self._paused == 0: 731 self._trySendMessages() 732 self._flushOutputs() 733 734 735 def addMessage(self, msg): 736 if self._stopped: 737 return 738 739 self.outgoingSeq += 1 740 self.messages.append((self.outgoingSeq, msg)) 741 if not self._paused and self.outputs: 742 output, timeout = self.outputs.pop(0) 743 timeout.cancel() 744 if not self.outputs: 745 self._transportlessTimeoutCall = self.scheduler(self.transportlessTimeout, self._transportlessTimedOut) 746 self._sendMessagesToOutput(output) 747 748 749 def addOutput(self, output): 750 if self._transportlessTimeoutCall is not None: 751 self._transportlessTimeoutCall.cancel() 752 self._transportlessTimeoutCall = None 753 if not self._paused and self.messages: 754 self._transportlessTimeoutCall = self.scheduler(self.transportlessTimeout, self._transportlessTimedOut) 755 self._sendMessagesToOutput(output) 756 else: 757 if self._stopped: 758 self._sendMessagesToOutput(output) 759 else: 760 self.outputs.append((output, self.scheduler(self.idleTimeout, self._idleTimedOut))) 761 762 763 def close(self): 764 assert not self._stopped, "Cannot multiply stop ReliableMessageDelivery" 765 self.addMessage((CLOSE, [])) 766 self._stopped = True 767 while self.outputs: 768 output, timeout = self.outputs.pop(0) 769 timeout.cancel() 770 self._sendMessagesToOutput(output) 771 self.outputs = None 772 if self._transportlessTimeoutCall is not None: 773 self._transportlessTimeoutCall.cancel() 774 self._transportlessTimeoutCall = None 775 776 777 def _unregisterDeferredAsOutputChannel(self, deferred): 778 for i in xrange(len(self.outputs)): 779 if self.outputs[i][0].im_self is deferred: 780 output, timeout = self.outputs.pop(i) 781 timeout.cancel() 782 break 783 else: 784 return 785 if not self.outputs: 786 self._transportlessTimeoutCall = self.scheduler(self.transportlessTimeout, self._transportlessTimedOut) 787 788 789 def _createOutputDeferred(self): 790 """ 791 Create a new deferred, attaching it as an output. If the current 792 state is not paused, try to flush any pending messages and expend 793 any excess outputs. 794 """ 795 d = defer.Deferred() 796 self.addOutput(d.callback) 797 if not self._paused and self.outputs: 798 self._trySendMessages() 799 self._flushOutputs() 800 return d 801 802 803 def _flushOutputs(self): 804 """ 805 Use up all except for one output. 806 807 This provides ideal behavior for the default HTTP client 808 configuration, since only a maximum of two simultaneous connections 809 are allowed. The remaining one output will let us signal the client 810 at will without preventing the client from establishing new 811 connections. 812 """ 813 if self.outputs is None: 814 return 815 while len(self.outputs) > 1: 816 output, timeout = self.outputs.pop(0) 817 timeout.cancel() 818 output([self.outgoingAck, []]) 819 820 821 def basketCaseReceived(self, ctx, basketCase): 822 """ 823 This is called when some random JSON data is received from an HTTP 824 request. 825 826 A 'basket case' is currently a data structure of the form [ackNum, [[1, 827 message], [2, message], [3, message]]] 828 829 Its name is highly informal because unless you are maintaining this 830 exact code path, you should not encounter it. If you do, something has 831 gone *badly* wrong. 832 """ 833 if not self._connected: 834 self._connected = True 835 self.connectionMade() 836 837 ack, incomingMessages = basketCase 838 839 outgoingMessages = self.messages 840 841 # dequeue messages that our client certainly knows about. 842 while outgoingMessages and outgoingMessages[0][0] <= ack: 843 outgoingMessages.pop(0) 844 845 if incomingMessages: 846 log.msg(athena_received_messages=True, count=len(incomingMessages)) 847 if incomingMessages[0][0] == UNLOAD: 848 # Page-unload messages are special, because they are not part 849 # of the normal message stream: they are a notification that 850 # the message stream can't continue. Browser bugs force us to 851 # handle this as quickly as possible, since the browser can 852 # lock up hard while waiting for a response to this message 853 # (and the user has already navigated away from the page, so 854 # there's no useful communication that can take place any more) 855 # so only one message is allowed. In the actual Athena JS, 856 # only one is ever sent, so there is no need to handle more. 857 # The structure of the packet is preserved for symmetry, 858 # however, if we ever need to expand on it. Realistically, the 859 # only message that can be usefully processed here is CLOSE. 860 msg = incomingMessages[0][1] 861 self.livePage.liveTransportMessageReceived(ctx, msg) 862 return self._createOutputDeferred() 863 elif self.outgoingAck + 1 >= incomingMessages[0][0]: 864 lastSentAck = self.outgoingAck 865 self.outgoingAck = max(incomingMessages[-1][0], self.outgoingAck) 866 self.pause() 867 try: 868 for (seq, msg) in incomingMessages: 869 if seq > lastSentAck: 870 self.livePage.liveTransportMessageReceived(ctx, msg) 871 d = self._createOutputDeferred() 872 finally: 873 self.unpause() 874 else: 875 d = defer.succeed([self.outgoingAck, []]) 876 log.msg( 877 "Sequence gap! %r went from %s to %s" % 878 (self.livePage.clientID, 879 self.outgoingAck, 880 incomingMessages[0][0])) 881 else: 882 d = self._createOutputDeferred() 883 884 return d 885 886 887BOOTSTRAP_NODE_ID = 'athena:bootstrap' 888BOOTSTRAP_STATEMENT = ("eval(document.getElementById('" + BOOTSTRAP_NODE_ID + 889 "').getAttribute('payload'));") 890 891class _HasJSClass(object): 892 """ 893 A utility to share some code between the L{LivePage}, L{LiveElement}, and 894 L{LiveFragment} classes which all have a jsClass attribute that represents 895 a JavaScript class. 896 897 @ivar jsClass: a JavaScript class. 898 @type jsClass: L{unicode} 899 """ 900 901 def _getModuleForClass(self): 902 """ 903 Get a L{JSModule} object for the class specified by this object's 904 jsClass string. 905 """ 906 return jsDeps.getModuleForClass(self.jsClass) 907 908 909 def _getRequiredModules(self, memo): 910 """ 911 Return a list of two-tuples containing module names and URLs at which 912 those modules are accessible. All of these modules must be loaded into 913 the page before this Fragment's widget can be instantiated. modules 914 are accessible. 915 """ 916 return [ 917 (dep.name, self.page.getJSModuleURL(dep.name)) 918 for dep 919 in self._getModuleForClass().allDependencies(memo) 920 if self.page._shouldInclude(dep.name)] 921 922 923 924def jsModuleDeclaration(name): 925 """ 926 Generate Javascript for a module declaration. 927 """ 928 var = '' 929 if '.' not in name: 930 var = 'var ' 931 return '%s%s = {"__name__": "%s"};' % (var, name, name) 932 933 934 935class _HasCSSModule(object): 936 """ 937 C{cssModule}-handling code common to L{LivePage}, L{LiveElement} and 938 L{LiveFragment}. 939 940 @ivar cssModule: A CSS module name. 941 @type cssModule: C{unicode} or C{NoneType} 942 """ 943 def _getRequiredCSSModules(self, memo): 944 """ 945 Return a list of CSS module URLs. 946 947 @rtype: C{list} of L{url.URL} 948 """ 949 if self.cssModule is None: 950 return [] 951 module = self.page.cssModules.getModuleForName(self.cssModule) 952 return [ 953 self.page.getCSSModuleURL(dep.name) 954 for dep in module.allDependencies(memo) 955 if self.page._shouldIncludeCSSModule(dep.name)] 956 957 958 def getStylesheetStan(self, modules): 959 """ 960 Get some stan which will include the given modules. 961 962 @type modules: C{list} or L{url.URL} 963 964 @rtype: Stan 965 """ 966 return [ 967 tags.link( 968 rel='stylesheet', type='text/css', href=url) 969 for url in modules] 970 971 972 973class LivePage(rend.Page, _HasJSClass, _HasCSSModule): 974 """ 975 A resource which can receive messages from and send messages to the client 976 after the initial page load has completed and which can send messages. 977 978 @ivar requiredBrowserVersions: A dictionary mapping User-Agent browser 979 names to the minimum supported version of those browsers. Clients 980 using these browsers which are below the minimum version will be shown 981 an alternate page explaining this rather than the normal page content. 982 983 @ivar unsupportedBrowserLoader: A document loader which will be used to 984 generate the content shown to unsupported browsers. 985 986 @type _cssDepsMemo: C{dict} 987 @ivar _cssDepsMemo: A cache for CSS module dependencies; by default, this 988 will only be shared within a single page instance. 989 990 @type _jsDepsMemo: C{dict} 991 @ivar _jsDepsMemo: A cache for JS module dependencies; by default, this 992 will only be shared within a single page instance. 993 994 @type _didConnect: C{bool} 995 @ivar _didConnect: Initially C{False}, set to C{True} if connectionMade has 996 been invoked. 997 998 @type _didDisconnect: C{bool} 999 @ivar _didDisconnect: Initially C{False}, set to C{True} if _disconnected 1000 has been invoked. 1001 1002 @type _localObjects: C{dict} of C{int} : widget 1003 @ivar _localObjects: Mapping from an object ID to a Python object that will 1004 accept messages from the client. 1005 1006 @type _localObjectIDCounter: C{callable} returning C{int} 1007 @ivar _localObjectIDCounter: A callable that will return a new 1008 locally-unique object ID each time it is called. 1009 """ 1010 jsClass = u'Nevow.Athena.PageWidget' 1011 cssModule = None 1012 1013 factory = LivePageFactory() 1014 _rendered = False 1015 _didConnect = False 1016 _didDisconnect = False 1017 1018 useActiveChannels = True 1019 1020 # This is the number of seconds that is acceptable for a LivePage to be 1021 # considered 'connected' without any transports still active. In other 1022 # words, if the browser cannot make requests for more than this timeout 1023 # (due to network problems, blocking javascript functions, or broken 1024 # proxies) then deferreds returned from notifyOnDisconnect() will be 1025 # errbacked with ConnectionLost, and the LivePage will be removed from the 1026 # factory's cache, and then likely garbage collected. 1027 TRANSPORTLESS_DISCONNECT_TIMEOUT = 30 1028 1029 # This is the amount of time that each 'transport' request will remain open 1030 # to the server. Although the underlying transport, i.e. the conceptual 1031 # connection established by the sequence of requests, remains alive, it is 1032 # necessary to periodically cancel requests to avoid browser and proxy 1033 # bugs. 1034 TRANSPORT_IDLE_TIMEOUT = 300 1035 1036 page = property(lambda self: self) 1037 1038 # Modules needed to bootstrap 1039 BOOTSTRAP_MODULES = ['Divmod', 'Divmod.Base', 'Divmod.Defer', 1040 'Divmod.Runtime', 'Nevow', 'Nevow.Athena'] 1041 1042 # Known minimum working versions of certain browsers. 1043 requiredBrowserVersions = { 1044 browsers.GECKO: (20051111,), 1045 browsers.INTERNET_EXPLORER: (6, 0), 1046 browsers.WEBKIT: (523,), 1047 browsers.OPERA: (9,)} 1048 1049 unsupportedBrowserLoader = loaders.stan( 1050 tags.html[ 1051 tags.body[ 1052 'Your browser is not supported by the Athena toolkit.']]) 1053 1054 1055 def __init__(self, iface=None, rootObject=None, jsModules=None, 1056 jsModuleRoot=None, transportRoot=None, cssModules=None, 1057 cssModuleRoot=None, *a, **kw): 1058 super(LivePage, self).__init__(*a, **kw) 1059 1060 self.iface = iface 1061 self.rootObject = rootObject 1062 if jsModules is None: 1063 jsModules = JSPackage(jsDeps.mapping) 1064 self.jsModules = jsModules 1065 self.jsModuleRoot = jsModuleRoot 1066 if transportRoot is None: 1067 transportRoot = here 1068 self.transportRoot = transportRoot 1069 self.cssModuleRoot = cssModuleRoot 1070 if cssModules is None: 1071 cssModules = _theCSSRegistry 1072 self.cssModules = cssModules 1073 self.liveFragmentChildren = [] 1074 self._includedModules = [] 1075 self._includedCSSModules = [] 1076 self._disconnectNotifications = [] 1077 self._jsDepsMemo = {} 1078 self._cssDepsMemo = {} 1079 1080 1081 def _shouldInclude(self, moduleName): 1082 if moduleName not in self._includedModules: 1083 self._includedModules.append(moduleName) 1084 return True 1085 return False 1086 1087 1088 def _shouldIncludeCSSModule(self, moduleName): 1089 """ 1090 Figure out whether the named CSS module has already been included. 1091 1092 @type moduleName: C{unicode} 1093 1094 @rtype: C{bool} 1095 """ 1096 if moduleName not in self._includedCSSModules: 1097 self._includedCSSModules.append(moduleName) 1098 return True 1099 return False 1100 1101 1102 # Child lookup may be dependent on the application state 1103 # represented by a LivePage. In this case, it is preferable to 1104 # dispatch child lookup on the same LivePage instance as performed 1105 # the initial rendering of the page. Override the default 1106 # implementation of locateChild to do this where appropriate. 1107 def locateChild(self, ctx, segments): 1108 try: 1109 client = self.factory.getClient(segments[0]) 1110 except KeyError: 1111 return super(LivePage, self).locateChild(ctx, segments) 1112 else: 1113 return client, segments[1:] 1114 1115 1116 def child___athena_private__(self, ctx): 1117 return _thePrivateAthenaResource 1118 1119 1120 # A note on timeout/disconnect logic: whenever a live client goes from some 1121 # transports to no transports, a timer starts; whenever it goes from no 1122 # transports to some transports, the timer is stopped; if the timer ever 1123 # expires the connection is considered lost; every time a transport is 1124 # added a timer is started; when the transport is used up, the timer is 1125 # stopped; if the timer ever expires, the transport has a no-op sent down 1126 # it; if an idle transport is ever disconnected, the connection is 1127 # considered lost; this lets the server notice clients who actively leave 1128 # (closed window, browser navigates away) and network congestion/errors 1129 # (unplugged ethernet cable, etc) 1130 def _becomeLive(self, location): 1131 """ 1132 Assign this LivePage a clientID, associate it with a factory, and begin 1133 tracking its state. This only occurs when a LivePage is *rendered*, 1134 not when it is instantiated. 1135 """ 1136 self.clientID = self.factory.addClient(self) 1137 1138 if self.jsModuleRoot is None: 1139 self.jsModuleRoot = location.child(self.clientID).child('jsmodule') 1140 if self.cssModuleRoot is None: 1141 self.cssModuleRoot = location.child(self.clientID).child('cssmodule') 1142 1143 self._requestIDCounter = itertools.count().next 1144 1145 self._messageDeliverer = ReliableMessageDelivery( 1146 self, 1147 self.TRANSPORTLESS_DISCONNECT_TIMEOUT * 2, 1148 self.TRANSPORTLESS_DISCONNECT_TIMEOUT, 1149 self.TRANSPORT_IDLE_TIMEOUT, 1150 self._disconnected, 1151 connectionMade=self._connectionMade) 1152 self._remoteCalls = {} 1153 self._localObjects = {} 1154 self._localObjectIDCounter = itertools.count().next 1155 1156 self.addLocalObject(self) 1157 1158 1159 def _supportedBrowser(self, request): 1160 """ 1161 Determine whether a known-unsupported browser is making a request. 1162 1163 @param request: The L{IRequest} being made. 1164 1165 @rtype: C{bool} 1166 @return: False if the user agent is known to be unsupported by Athena, 1167 True otherwise. 1168 """ 1169 agentString = request.getHeader("user-agent") 1170 if agentString is None: 1171 return True 1172 agent = UserAgent.fromHeaderValue(agentString) 1173 if agent is None: 1174 return True 1175 1176 requiredVersion = self.requiredBrowserVersions.get(agent.browser, None) 1177 if requiredVersion is not None: 1178 return agent.version >= requiredVersion 1179 return True 1180 1181 1182 def renderUnsupported(self, ctx): 1183 """ 1184 Render a notification to the user that his user agent is 1185 unsupported by this LivePage. 1186 1187 @param ctx: The current rendering context. 1188 1189 @return: Something renderable (same behavior as L{renderHTTP}) 1190 """ 1191 return flat.flatten(self.unsupportedBrowserLoader.load()) 1192 1193 1194 def renderHTTP(self, ctx): 1195 """ 1196 Attach this livepage to its transport, and render it and all of its 1197 attached widgets to the browser. During rendering, the page is 1198 attached to its factory, acquires a clientID, and has headers set 1199 appropriately to prevent a browser from ever caching the page, since 1200 the clientID it gives to the browser is transient and changes every 1201 time. 1202 1203 These state changes associated with rendering mean that L{LivePage}s 1204 can only be rendered once, because they are attached to a particular 1205 user's browser, and it must be unambiguous what browser 1206 L{LivePage.callRemote} will invoke the method in. 1207 1208 The page's contents are rendered according to its docFactory, as with a 1209 L{Page}, unless the user-agent requesting this LivePage is determined 1210 to be unsupported by the JavaScript runtime required by Athena. In 1211 that case, a static page is rendered by this page's 1212 C{renderUnsupported} method. 1213 1214 If a special query argument is set in the URL, "__athena_reconnect__", 1215 the page will instead render the JSON-encoded clientID by itself as the 1216 page's content. This allows an existing live page in a browser to 1217 programmatically reconnect without re-rendering and re-loading the 1218 entire page. 1219 1220 @see: L{LivePage.renderUnsupported} 1221 1222 @see: L{Page.renderHTTP} 1223 1224 @param ctx: a L{WovenContext} with L{IRequest} remembered. 1225 1226 @return: a string (the content of the page) or a Deferred which will 1227 fire with the same. 1228 1229 @raise RuntimeError: if the page has already been rendered, or this 1230 page has not been given a factory. 1231 """ 1232 if self._rendered: 1233 raise RuntimeError("Cannot render a LivePage more than once") 1234 if self.factory is None: 1235 raise RuntimeError("Cannot render a LivePage without a factory") 1236 1237 self._rendered = True 1238 request = inevow.IRequest(ctx) 1239 if not self._supportedBrowser(request): 1240 request.write(self.renderUnsupported(ctx)) 1241 return '' 1242 1243 self._becomeLive(URL.fromString(flat.flatten(here, ctx))) 1244 1245 neverEverCache(request) 1246 if request.args.get(ATHENA_RECONNECT): 1247 return json.serialize(self.clientID.decode("ascii")) 1248 return rend.Page.renderHTTP(self, ctx) 1249 1250 1251 def _connectionMade(self): 1252 """ 1253 Invoke connectionMade on all attached widgets. 1254 """ 1255 for widget in self._localObjects.values(): 1256 widget.connectionMade() 1257 self._didConnect = True 1258 1259 1260 def _disconnected(self, reason): 1261 """ 1262 Callback invoked when the L{ReliableMessageDelivery} is disconnected. 1263 1264 If the page has not already disconnected, fire any deferreds created 1265 with L{notifyOnDisconnect}; if the page was already connected, fire 1266 C{connectionLost} methods on attached widgets. 1267 """ 1268 if not self._didDisconnect: 1269 self._didDisconnect = True 1270 1271 notifications = self._disconnectNotifications 1272 self._disconnectNotifications = None 1273 for d in notifications: 1274 d.errback(reason) 1275 calls = self._remoteCalls 1276 self._remoteCalls = {} 1277 for (reqID, resD) in calls.iteritems(): 1278 resD.errback(reason) 1279 if self._didConnect: 1280 for widget in self._localObjects.values(): 1281 widget.connectionLost(reason) 1282 self.factory.removeClient(self.clientID) 1283 1284 1285 def connectionMade(self): 1286 """ 1287 Callback invoked when the transport is first connected. 1288 """ 1289 1290 1291 def connectionLost(self, reason): 1292 """ 1293 Callback invoked when the transport is disconnected. 1294 1295 This method will only be called if connectionMade was called. 1296 1297 Override this. 1298 """ 1299 1300 1301 def addLocalObject(self, obj): 1302 objID = self._localObjectIDCounter() 1303 self._localObjects[objID] = obj 1304 return objID 1305 1306 1307 def removeLocalObject(self, objID): 1308 """ 1309 Remove an object from the page's mapping of IDs that can receive 1310 messages. 1311 1312 @type objID: C{int} 1313 @param objID: The ID returned by L{LivePage.addLocalObject}. 1314 """ 1315 del self._localObjects[objID] 1316 1317 1318 def callRemote(self, methodName, *args): 1319 requestID = u's2c%i' % (self._requestIDCounter(),) 1320 message = (u'call', (unicode(methodName, 'ascii'), requestID, args)) 1321 resultD = defer.Deferred() 1322 self._remoteCalls[requestID] = resultD 1323 self.addMessage(message) 1324 return resultD 1325 1326 1327 def addMessage(self, message): 1328 self._messageDeliverer.addMessage(message) 1329 1330 1331 def notifyOnDisconnect(self): 1332 """ 1333 Return a Deferred which will fire or errback when this LivePage is 1334 no longer connected. 1335 1336 Note that if a LivePage never establishes a connection in the first 1337 place, the Deferreds this returns will never fire. 1338 1339 @rtype: L{defer.Deferred} 1340 """ 1341 d = defer.Deferred() 1342 self._disconnectNotifications.append(d) 1343 return d 1344 1345 1346 def getJSModuleURL(self, moduleName): 1347 return self.jsModuleRoot.child(moduleName) 1348 1349 1350 def getCSSModuleURL(self, moduleName): 1351 """ 1352 Return a URL rooted a L{cssModuleRoot} from which the CSS module named 1353 C{moduleName} can be fetched. 1354 1355 @type moduleName: C{unicode} 1356 1357 @rtype: C{str} 1358 """ 1359 return self.cssModuleRoot.child(moduleName) 1360 1361 1362 def getImportStan(self, moduleName): 1363 moduleDef = jsModuleDeclaration(moduleName); 1364 return [tags.script(type='text/javascript')[tags.raw(moduleDef)], 1365 tags.script(type='text/javascript', src=self.getJSModuleURL(moduleName))] 1366 1367 1368 def render_liveglue(self, ctx, data): 1369 bootstrapString = '\n'.join( 1370 [self._bootstrapCall(method, args) for 1371 method, args in self._bootstraps(ctx)]) 1372 return ctx.tag[ 1373 self.getStylesheetStan(self._getRequiredCSSModules(self._cssDepsMemo)), 1374 1375 # Hit jsDeps.getModuleForName to force it to load some plugins :/ 1376 # This really needs to be redesigned. 1377 [self.getImportStan(jsDeps.getModuleForName(name).name) 1378 for (name, url) 1379 in self._getRequiredModules(self._jsDepsMemo)], 1380 tags.script(type='text/javascript', 1381 id=BOOTSTRAP_NODE_ID, 1382 payload=bootstrapString)[ 1383 BOOTSTRAP_STATEMENT] 1384 ] 1385 1386 1387 def _bootstraps(self, ctx): 1388 """ 1389 Generate a list of 2-tuples of (methodName, arguments) representing the 1390 methods which need to be invoked as soon as all the bootstrap modules 1391 are loaded. 1392 1393 @param: a L{WovenContext} that can render an URL. 1394 """ 1395 return [ 1396 ("Divmod.bootstrap", 1397 [flat.flatten(self.transportRoot, ctx).decode("ascii")]), 1398 ("Nevow.Athena.bootstrap", 1399 [self.jsClass, self.clientID.decode('ascii')])] 1400 1401 1402 def _bootstrapCall(self, methodName, args): 1403 """ 1404 Generate a string to call a 'bootstrap' function in an Athena JavaScript 1405 module client-side. 1406 1407 @param methodName: the name of the method. 1408 1409 @param args: a list of objects that will be JSON-serialized as 1410 arguments to the named method. 1411 """ 1412 return '%s(%s);' % ( 1413 methodName, ', '.join([json.serialize(arg) for arg in args])) 1414 1415 1416 def child_jsmodule(self, ctx): 1417 return MappingResource(self.jsModules.mapping) 1418 1419 1420 def child_cssmodule(self, ctx): 1421 """ 1422 Return a L{MappingResource} wrapped around L{cssModules}. 1423 """ 1424 return MappingResource(self.cssModules.mapping) 1425 1426 1427 _transportResource = None 1428 def child_transport(self, ctx): 1429 if self._transportResource is None: 1430 self._transportResource = LivePageTransport( 1431 self._messageDeliverer, 1432 self.useActiveChannels) 1433 return self._transportResource 1434 1435 1436 def locateMethod(self, ctx, methodName): 1437 if methodName in self.iface: 1438 return getattr(self.rootObject, methodName) 1439 raise AttributeError(methodName) 1440 1441 1442 def liveTransportMessageReceived(self, ctx, (action, args)): 1443 """ 1444 A message was received from the reliable transport layer. Process it by 1445 dispatching it first to myself, then later to application code if 1446 applicable. 1447 """ 1448 method = getattr(self, 'action_' + action) 1449 method(ctx, *args) 1450 1451 1452 def action_call(self, ctx, requestId, method, objectID, args, kwargs): 1453 """ 1454 Handle a remote call initiated by the client. 1455 """ 1456 localObj = self._localObjects[objectID] 1457 try: 1458 func = localObj.locateMethod(ctx, method) 1459 except AttributeError: 1460 result = defer.fail(NoSuchMethod(objectID, method)) 1461 else: 1462 result = defer.maybeDeferred(func, *args, **kwargs) 1463 def _cbCall(result): 1464 success = True 1465 if isinstance(result, failure.Failure): 1466 log.msg("Sending error to browser:") 1467 log.err(result) 1468 success = False 1469 if result.check(LivePageError): 1470 result = ( 1471 result.value.jsClass, 1472 result.value.args) 1473 else: 1474 result = ( 1475 u'Divmod.Error', 1476 [u'%s: %s' % ( 1477 result.type.__name__.decode('ascii'), 1478 result.getErrorMessage().decode('ascii'))]) 1479 message = (u'respond', (unicode(requestId), success, result)) 1480 self.addMessage(message) 1481 result.addBoth(_cbCall) 1482 1483 1484 def action_respond(self, ctx, responseId, success, result): 1485 """ 1486 Handle the response from the client to a call initiated by the server. 1487 """ 1488 callDeferred = self._remoteCalls.pop(responseId) 1489 if success: 1490 callDeferred.callback(result) 1491 else: 1492 callDeferred.errback(getJSFailure(result, self.jsModules.mapping)) 1493 1494 1495 def action_noop(self, ctx): 1496 """ 1497 Handle noop, used to initialise and ping the live transport. 1498 """ 1499 1500 1501 def action_close(self, ctx): 1502 """ 1503 The client is going away. Clean up after them. 1504 """ 1505 self._messageDeliverer.close() 1506 self._disconnected(error.ConnectionDone("Connection closed")) 1507 1508 1509 1510handler = stan.Proto('athena:handler') 1511_handlerFormat = "return Nevow.Athena.Widget.handleEvent(this, %(event)s, %(handler)s);" 1512 1513def _rewriteEventHandlerToAttribute(tag): 1514 """ 1515 Replace athena:handler children of the given tag with attributes on the tag 1516 which correspond to those event handlers. 1517 """ 1518 if isinstance(tag, stan.Tag): 1519 extraAttributes = {} 1520 for i in xrange(len(tag.children) - 1, -1, -1): 1521 if isinstance(tag.children[i], stan.Tag) and tag.children[i].tagName == 'athena:handler': 1522 info = tag.children.pop(i) 1523 name = info.attributes['event'].encode('ascii') 1524 handler = info.attributes['handler'] 1525 extraAttributes[name] = _handlerFormat % { 1526 'handler': json.serialize(handler.decode('ascii')), 1527 'event': json.serialize(name.decode('ascii'))} 1528 tag(**extraAttributes) 1529 return tag 1530 1531 1532def rewriteEventHandlerNodes(root): 1533 """ 1534 Replace all the athena:handler nodes in a given document with onfoo 1535 attributes. 1536 """ 1537 stan.visit(root, _rewriteEventHandlerToAttribute) 1538 return root 1539 1540 1541def _mangleId(oldId): 1542 """ 1543 Return a consistently mangled form of an id that is unique to the widget 1544 within which it occurs. 1545 """ 1546 return ['athenaid:', tags.slot('athena:id'), '-', oldId] 1547 1548 1549def _rewriteAthenaId(tag): 1550 """ 1551 Rewrite id attributes to be prefixed with the ID of the widget the node is 1552 contained by. Also rewrite label "for" attributes which must match the id of 1553 their form element. 1554 """ 1555 if isinstance(tag, stan.Tag): 1556 elementId = tag.attributes.pop('id', None) 1557 if elementId is not None: 1558 tag.attributes['id'] = _mangleId(elementId) 1559 if tag.tagName == "label": 1560 elementFor = tag.attributes.pop('for', None) 1561 if elementFor is not None: 1562 tag.attributes['for'] = _mangleId(elementFor) 1563 if tag.tagName in ('td', 'th'): 1564 headers = tag.attributes.pop('headers', None) 1565 if headers is not None: 1566 ids = headers.split() 1567 headers = [_mangleId(headerId) for headerId in ids] 1568 for n in xrange(len(headers) - 1, 0, -1): 1569 headers.insert(n, ' ') 1570 tag.attributes['headers'] = headers 1571 return tag 1572 1573 1574def rewriteAthenaIds(root): 1575 """ 1576 Rewrite id attributes to be unique to the widget they're in. 1577 """ 1578 stan.visit(root, _rewriteAthenaId) 1579 return root 1580 1581 1582class _LiveMixin(_HasJSClass, _HasCSSModule): 1583 jsClass = u'Nevow.Athena.Widget' 1584 cssModule = None 1585 1586 preprocessors = [rewriteEventHandlerNodes, rewriteAthenaIds] 1587 1588 fragmentParent = None 1589 1590 _page = None 1591 1592 # Reference to the result of a call to _structured, if one has been made, 1593 # otherwise None. This is used to make _structured() idempotent. 1594 _structuredCache = None 1595 1596 def __init__(self, *a, **k): 1597 super(_LiveMixin, self).__init__(*a, **k) 1598 self.liveFragmentChildren = [] 1599 1600 def page(): 1601 def get(self): 1602 if self._page is None: 1603 if self.fragmentParent is not None: 1604 self._page = self.fragmentParent.page 1605 return self._page 1606 def set(self, value): 1607 self._page = value 1608 doc = """ 1609 The L{LivePage} instance which is the topmost container of this 1610 fragment. 1611 """ 1612 return get, set, None, doc 1613 page = property(*page()) 1614 1615 1616 def getInitialArguments(self): 1617 """ 1618 Return a C{tuple} or C{list} of arguments to be passed to this 1619 C{LiveFragment}'s client-side Widget. 1620 1621 This will be called during the rendering process. Whatever it 1622 returns will be serialized into the page and passed to the 1623 C{__init__} method of the widget specified by C{jsClass}. 1624 1625 @rtype: C{list} or C{tuple} 1626 """ 1627 return () 1628 1629 1630 def _prepare(self, tag): 1631 """ 1632 Check for clearly incorrect settings of C{self.jsClass} and 1633 C{self.page}, add this object to the page and fill the I{athena:id} 1634 slot with this object's Athena identifier. 1635 """ 1636 assert isinstance(self.jsClass, unicode), "jsClass must be a unicode string" 1637 1638 if self.page is None: 1639 raise OrphanedFragment(self) 1640 self._athenaID = self.page.addLocalObject(self) 1641 if self.page._didConnect: 1642 self.connectionMade() 1643 tag.fillSlots('athena:id', str(self._athenaID)) 1644 1645 1646 def setFragmentParent(self, fragmentParent): 1647 """ 1648 Sets the L{LiveFragment} (or L{LivePage}) which is the logical parent 1649 of this fragment. This should parallel the client-side hierarchy. 1650 1651 All LiveFragments must have setFragmentParent called on them before 1652 they are rendered for the client; otherwise, they will be unable to 1653 properly hook up to the page. 1654 1655 LiveFragments should have their own setFragmentParent called before 1656 calling setFragmentParent on any of their own children. The normal way 1657 to accomplish this is to instantiate your fragment children during the 1658 render pass. 1659 1660 If that isn't feasible, instead override setFragmentParent and 1661 instantiate your children there. 1662 1663 This architecture might seem contorted, but what it allows that is 1664 interesting is adaptation of foreign objects to LiveFragment. Anywhere 1665 you adapt to LiveFragment, setFragmentParent is the next thing that 1666 should be called. 1667 """ 1668 self.fragmentParent = fragmentParent 1669 self.page = fragmentParent.page 1670 fragmentParent.liveFragmentChildren.append(self) 1671 1672 1673 def _flatten(self, what): 1674 """ 1675 Synchronously flatten C{what} and return the result as a C{str}. 1676 """ 1677 # Nested import because in a significant stroke of misfortune, 1678 # nevow.testutil already depends on nevow.athena. It makes more sense 1679 # for the dependency to go from nevow.athena to nevow.testutil. 1680 # Perhaps a sane way to fix this would be to move FakeRequest to a 1681 # different module from whence nevow.athena and nevow.testutil could 1682 # import it. -exarkun 1683 from nevow.testutil import FakeRequest 1684 s = StringIO.StringIO() 1685 for _ in _flat.flatten(FakeRequest(), s.write, what, False, False): 1686 pass 1687 return s.getvalue() 1688 1689 1690 def _structured(self): 1691 """ 1692 Retrieve an opaque object which may be usable to construct the 1693 client-side Widgets which correspond to this fragment and all of its 1694 children. 1695 """ 1696 if self._structuredCache is not None: 1697 return self._structuredCache 1698 1699 children = [] 1700 requiredModules = [] 1701 requiredCSSModules = [] 1702 1703 # Using the context here is terrible but basically necessary given the 1704 # /current/ architecture of Athena and flattening. A better 1705 # implementation which was not tied to the rendering system could avoid 1706 # this. 1707 markup = context.call( 1708 {'children': children, 1709 'requiredModules': requiredModules, 1710 'requiredCSSModules': requiredCSSModules}, 1711 self._flatten, tags.div(xmlns="http://www.w3.org/1999/xhtml")[self]).decode('utf-8') 1712 1713 del children[0] 1714 1715 self._structuredCache = { 1716 u'requiredModules': [(name, flat.flatten(url).decode('utf-8')) 1717 for (name, url) in requiredModules], 1718 u'requiredCSSModules': [flat.flatten(url).decode('utf-8') 1719 for url in requiredCSSModules], 1720 u'class': self.jsClass, 1721 u'id': self._athenaID, 1722 u'initArguments': tuple(self.getInitialArguments()), 1723 u'markup': markup, 1724 u'children': children} 1725 return self._structuredCache 1726 1727 1728 def liveElement(self, request, tag): 1729 """ 1730 Render framework-level boilerplate for making sure the Widget for this 1731 Element is created and added to the page properly. 1732 """ 1733 requiredModules = self._getRequiredModules(self.page._jsDepsMemo) 1734 requiredCSSModules = self._getRequiredCSSModules(self.page._cssDepsMemo) 1735 1736 # Add required attributes to the top widget node 1737 tag(**{'xmlns:athena': ATHENA_XMLNS_URI, 1738 'id': 'athena:%d' % self._athenaID, 1739 'athena:class': self.jsClass}) 1740 1741 # This will only be set if _structured() is being run. 1742 if context.get('children') is not None: 1743 context.get('children').append({ 1744 u'class': self.jsClass, 1745 u'id': self._athenaID, 1746 u'initArguments': self.getInitialArguments()}) 1747 context.get('requiredModules').extend(requiredModules) 1748 context.get('requiredCSSModules').extend(requiredCSSModules) 1749 return tag 1750 1751 return ( 1752 self.getStylesheetStan(requiredCSSModules), 1753 1754 # Import stuff 1755 [self.getImportStan(name) for (name, url) in requiredModules], 1756 1757 # Dump some data for our client-side __init__ into a text area 1758 # where it can easily be found. 1759 tags.textarea(id='athena-init-args-' + str(self._athenaID), 1760 style="display: none")[ 1761 json.serialize(self.getInitialArguments())], 1762 1763 # Arrange to be instantiated 1764 tags.script(type='text/javascript')[ 1765 """ 1766 Nevow.Athena.Widget._widgetNodeAdded(%(athenaID)d); 1767 """ % {'athenaID': self._athenaID, 1768 'pythonClass': self.__class__.__name__}], 1769 1770 # Okay, application stuff, plus metadata 1771 tag, 1772 ) 1773 renderer(liveElement) 1774 1775 1776 def render_liveFragment(self, ctx, data): 1777 return self.liveElement(inevow.IRequest(ctx), ctx.tag) 1778 1779 1780 def getImportStan(self, moduleName): 1781 return self.page.getImportStan(moduleName) 1782 1783 1784 def locateMethod(self, ctx, methodName): 1785 remoteMethod = expose.get(self, methodName, None) 1786 if remoteMethod is None: 1787 raise AttributeError(self, methodName) 1788 return remoteMethod 1789 1790 1791 def callRemote(self, methodName, *varargs): 1792 return self.page.callRemote( 1793 "Nevow.Athena.callByAthenaID", 1794 self._athenaID, 1795 unicode(methodName, 'ascii'), 1796 varargs) 1797 1798 1799 def _athenaDetachServer(self): 1800 """ 1801 Locally remove this from its parent. 1802 1803 @raise OrphanedFragment: if not attached to a parent. 1804 """ 1805 if self.fragmentParent is None: 1806 raise OrphanedFragment(self) 1807 for ch in list(self.liveFragmentChildren): 1808 ch._athenaDetachServer() 1809 self.fragmentParent.liveFragmentChildren.remove(self) 1810 self.fragmentParent = None 1811 page = self.page 1812 self.page = None 1813 page.removeLocalObject(self._athenaID) 1814 if page._didConnect: 1815 self.connectionLost(ConnectionLost('Detached')) 1816 self.detached() 1817 expose(_athenaDetachServer) 1818 1819 1820 def detach(self): 1821 """ 1822 Remove this from its parent after notifying the client that this is 1823 happening. 1824 1825 This function will *not* work correctly if the parent/child 1826 relationships of this widget do not exactly match the parent/child 1827 relationships of the corresponding fragments or elements on the server. 1828 1829 @return: A L{Deferred} which will fire when the detach completes. 1830 """ 1831 d = self.callRemote('_athenaDetachClient') 1832 d.addCallback(lambda ign: self._athenaDetachServer()) 1833 return d 1834 1835 1836 def detached(self): 1837 """ 1838 Application-level callback invoked when L{detach} succeeds or when the 1839 client invokes the detach logic from its side. 1840 1841 This is invoked after this fragment has been disassociated from its 1842 parent and from the page. 1843 1844 Override this. 1845 """ 1846 1847 1848 def connectionMade(self): 1849 """ 1850 Callback invoked when the transport is first available to this widget. 1851 1852 Override this. 1853 """ 1854 1855 1856 def connectionLost(self, reason): 1857 """ 1858 Callback invoked once the transport is no longer available to this 1859 widget. 1860 1861 This method will only be called if connectionMade was called. 1862 1863 Override this. 1864 """ 1865 1866 1867 1868class LiveFragment(_LiveMixin, rend.Fragment): 1869 """ 1870 This class is deprecated because it relies on context objects 1871 U{which are being removed from Nevow<http://divmod.org/trac/wiki/WitherContext>}. 1872 1873 @see: L{LiveElement} 1874 """ 1875 def __init__(self, *a, **kw): 1876 super(LiveFragment, self).__init__(*a, **kw) 1877 warnings.warn("[v0.10] LiveFragment has been superceded by LiveElement.", 1878 category=DeprecationWarning, 1879 stacklevel=2) 1880 1881 1882 def rend(self, context, data): 1883 """ 1884 Hook into the rendering process in order to check preconditions and 1885 make sure the document will actually be renderable by satisfying 1886 certain Athena requirements. 1887 """ 1888 context = rend.Fragment.rend(self, context, data) 1889 self._prepare(context.tag) 1890 return context 1891 1892 1893 1894 1895class LiveElement(_LiveMixin, Element): 1896 """ 1897 Base-class for a portion of a LivePage. When being rendered, a LiveElement 1898 has a special ID attribute added to its top-level tag. This attribute is 1899 used to dispatch calls from the client onto the correct object (this one). 1900 1901 A LiveElement must use the `liveElement' renderer somewhere in its document 1902 template. The node given this renderer will be the node used to construct 1903 a Widget instance in the browser (where it will be saved as the `node' 1904 property on the widget object). 1905 1906 JavaScript handlers for elements inside this node can use 1907 C{Nevow.Athena.Widget.get} to retrieve the widget associated with this 1908 LiveElement. For example:: 1909 1910 <form onsubmit="Nevow.Athena.Widget.get(this).callRemote('foo', bar); return false;"> 1911 1912 Methods of the JavaScript widget class can also be bound as event handlers 1913 using the handler tag type in the Athena namespace:: 1914 1915 <form xmlns:athena="http://divmod.org/ns/athena/0.7"> 1916 <athena:handler event="onsubmit" handler="doFoo" /> 1917 </form> 1918 1919 This will invoke the C{doFoo} method of the widget which contains the form 1920 node. 1921 1922 Because this mechanism sets up error handling and otherwise reduces the 1923 required boilerplate for handling events, it is preferred and recommended 1924 over directly including JavaScript in the event handler attribute of a 1925 node. 1926 1927 The C{jsClass} attribute of a LiveElement instance determines the 1928 JavaScript class used to construct its corresponding Widget. This appears 1929 as the 'athena:class' attribute. 1930 1931 JavaScript modules may import other JavaScript modules by using a special 1932 comment which Athena recognizes:: 1933 1934 // import Module.Name 1935 1936 Different imports must be placed on different lines. No other comment 1937 style is supported for these directives. Only one space character must 1938 appear between the string 'import' and the name of the module to be 1939 imported. No trailing whitespace or non-whitespace is allowed. There must 1940 be exactly one space between '//' and 'import'. There must be no 1941 preceeding whitespace on the line. 1942 1943 C{Nevow.Athena.Widget.callRemote} can be given permission to invoke methods 1944 on L{LiveElement} instances by passing the functions which implement those 1945 methods to L{nevow.athena.expose} in this way:: 1946 1947 class SomeElement(LiveElement): 1948 def someMethod(self, ...): 1949 ... 1950 expose(someMethod) 1951 1952 Only methods exposed in this way will be accessible. 1953 1954 L{LiveElement.callRemote} can be used to invoke any method of the widget on 1955 the client. 1956 1957 XML elements with id attributes will be rewritten so that the id is unique 1958 to that particular instance. The client-side 1959 C{Nevow.Athena.Widget.nodeById} API is provided to locate these later 1960 on. For example:: 1961 1962 <div id="foo" /> 1963 1964 and then:: 1965 1966 var node = self.nodyById('foo'); 1967 1968 On most platforms, this API will be much faster than similar techniques 1969 using C{Nevow.Athena.Widget.nodeByAttribute} etc. 1970 1971 Similarly to how Javascript classes are specified, L{LiveElement} 1972 instances may also identify a CSS module which provides appropriate styles 1973 with the C{cssModule} attribute (a unicode string naming a module within a 1974 L{inevow.ICSSPackage}). 1975 1976 The referenced CSS modules are treated as regular CSS, with the exception 1977 of support for the same:: 1978 1979 // import CSSModule.Name 1980 1981 syntax as is provided for Javascript modules. 1982 """ 1983 def render(self, request): 1984 """ 1985 Hook into the rendering process in order to check preconditions and 1986 make sure the document will actually be renderable by satisfying 1987 certain Athena requirements. 1988 """ 1989 document = tags.invisible[Element.render(self, request)] 1990 self._prepare(document) 1991 return document 1992 1993 1994 1995class IntrospectionFragment(LiveFragment): 1996 """ 1997 Utility for developers which provides detailed information about 1998 the state of a live page. 1999 """ 2000 2001 jsClass = u'Nevow.Athena.IntrospectionWidget' 2002 2003 docFactory = loaders.stan( 2004 tags.span(render=tags.directive('liveFragment'))[ 2005 tags.a( 2006 href="#DEBUG_ME", 2007 class_='toggle-debug')["Debug"]]) 2008 2009 2010 2011__all__ = [ 2012 # Constants 2013 'ATHENA_XMLNS_URI', 2014 2015 # Errors 2016 'LivePageError', 'OrphanedFragment', 'ConnectFailed', 'ConnectionLost', 2017 2018 # JS support 2019 'MappingResource', 'JSModule', 'JSPackage', 'AutoJSPackage', 2020 'allJavascriptPackages', 'JSDependencies', 'JSException', 'JSCode', 2021 'JSFrame', 'JSTraceback', 2022 2023 # CSS support 2024 'CSSRegistry', 'CSSModule', 2025 2026 # Core objects 2027 'LivePage', 'LiveFragment', 'LiveElement', 'IntrospectionFragment', 2028 2029 # Decorators 2030 'expose', 'handler', 2031 ] 2032