1# Copyright (c) Twisted Matrix Laboratories. 2# See LICENSE for details. 3 4""" 5twisted.web.util and twisted.web.template merged to avoid cyclic deps 6""" 7 8import io 9import linecache 10import warnings 11from collections import OrderedDict 12from html import escape 13from typing import ( 14 IO, 15 Any, 16 AnyStr, 17 Callable, 18 Dict, 19 List, 20 Mapping, 21 Optional, 22 Tuple, 23 Union, 24 cast, 25) 26from xml.sax import handler, make_parser 27from xml.sax.xmlreader import Locator 28 29from zope.interface import implementer 30 31from twisted.internet.defer import Deferred 32from twisted.logger import Logger 33from twisted.python import urlpath 34from twisted.python.failure import Failure 35from twisted.python.filepath import FilePath 36from twisted.python.reflect import fullyQualifiedName 37from twisted.web import resource 38from twisted.web._element import Element, renderer 39from twisted.web._flatten import Flattenable, flatten, flattenString 40from twisted.web._stan import CDATA, Comment, Tag, slot 41from twisted.web.iweb import IRenderable, IRequest, ITemplateLoader 42 43 44def _PRE(text): 45 """ 46 Wraps <pre> tags around some text and HTML-escape it. 47 48 This is here since once twisted.web.html was deprecated it was hard to 49 migrate the html.PRE from current code to twisted.web.template. 50 51 For new code consider using twisted.web.template. 52 53 @return: Escaped text wrapped in <pre> tags. 54 @rtype: C{str} 55 """ 56 return f"<pre>{escape(text)}</pre>" 57 58 59def redirectTo(URL: bytes, request: IRequest) -> bytes: 60 """ 61 Generate a redirect to the given location. 62 63 @param URL: A L{bytes} giving the location to which to redirect. 64 65 @param request: The request object to use to generate the redirect. 66 @type request: L{IRequest<twisted.web.iweb.IRequest>} provider 67 68 @raise TypeError: If the type of C{URL} a L{str} instead of L{bytes}. 69 70 @return: A L{bytes} containing HTML which tries to convince the client 71 agent 72 to visit the new location even if it doesn't respect the I{FOUND} 73 response code. This is intended to be returned from a render method, 74 eg:: 75 76 def render_GET(self, request): 77 return redirectTo(b"http://example.com/", request) 78 """ 79 if not isinstance(URL, bytes): 80 raise TypeError("URL must be bytes") 81 request.setHeader(b"Content-Type", b"text/html; charset=utf-8") 82 request.redirect(URL) 83 # FIXME: The URL should be HTML-escaped. 84 # https://twistedmatrix.com/trac/ticket/9839 85 content = b""" 86<html> 87 <head> 88 <meta http-equiv=\"refresh\" content=\"0;URL=%(url)s\"> 89 </head> 90 <body bgcolor=\"#FFFFFF\" text=\"#000000\"> 91 <a href=\"%(url)s\">click here</a> 92 </body> 93</html> 94""" % { 95 b"url": URL 96 } 97 return content 98 99 100class Redirect(resource.Resource): 101 """ 102 Resource that redirects to a specific URL. 103 104 @ivar url: Redirect target URL to put in the I{Location} response header. 105 @type url: L{bytes} 106 """ 107 108 isLeaf = True 109 110 def __init__(self, url: bytes): 111 super().__init__() 112 self.url = url 113 114 def render(self, request): 115 return redirectTo(self.url, request) 116 117 def getChild(self, name, request): 118 return self 119 120 121# FIXME: This is totally broken, see https://twistedmatrix.com/trac/ticket/9838 122class ChildRedirector(Redirect): 123 isLeaf = False 124 125 def __init__(self, url): 126 # XXX is this enough? 127 if ( 128 (url.find("://") == -1) 129 and (not url.startswith("..")) 130 and (not url.startswith("/")) 131 ): 132 raise ValueError( 133 ( 134 "It seems you've given me a redirect (%s) that is a child of" 135 " myself! That's not good, it'll cause an infinite redirect." 136 ) 137 % url 138 ) 139 Redirect.__init__(self, url) 140 141 def getChild(self, name, request): 142 newUrl = self.url 143 if not newUrl.endswith("/"): 144 newUrl += "/" 145 newUrl += name 146 return ChildRedirector(newUrl) 147 148 149class ParentRedirect(resource.Resource): 150 """ 151 Redirect to the nearest directory and strip any query string. 152 153 This generates redirects like:: 154 155 / \u2192 / 156 /foo \u2192 / 157 /foo?bar \u2192 / 158 /foo/ \u2192 /foo/ 159 /foo/bar \u2192 /foo/ 160 /foo/bar?baz \u2192 /foo/ 161 162 However, the generated I{Location} header contains an absolute URL rather 163 than a path. 164 165 The response is the same regardless of HTTP method. 166 """ 167 168 isLeaf = 1 169 170 def render(self, request: IRequest) -> bytes: 171 """ 172 Respond to all requests by redirecting to nearest directory. 173 """ 174 here = str(urlpath.URLPath.fromRequest(request).here()).encode("ascii") 175 return redirectTo(here, request) 176 177 178class DeferredResource(resource.Resource): 179 """ 180 I wrap up a Deferred that will eventually result in a Resource 181 object. 182 """ 183 184 isLeaf = 1 185 186 def __init__(self, d): 187 resource.Resource.__init__(self) 188 self.d = d 189 190 def getChild(self, name, request): 191 return self 192 193 def render(self, request): 194 self.d.addCallback(self._cbChild, request).addErrback(self._ebChild, request) 195 from twisted.web.server import NOT_DONE_YET 196 197 return NOT_DONE_YET 198 199 def _cbChild(self, child, request): 200 request.render(resource.getChildForRequest(child, request)) 201 202 def _ebChild(self, reason, request): 203 request.processingFailed(reason) 204 205 206class _SourceLineElement(Element): 207 """ 208 L{_SourceLineElement} is an L{IRenderable} which can render a single line of 209 source code. 210 211 @ivar number: A C{int} giving the line number of the source code to be 212 rendered. 213 @ivar source: A C{str} giving the source code to be rendered. 214 """ 215 216 def __init__(self, loader, number, source): 217 Element.__init__(self, loader) 218 self.number = number 219 self.source = source 220 221 @renderer 222 def sourceLine(self, request, tag): 223 """ 224 Render the line of source as a child of C{tag}. 225 """ 226 return tag(self.source.replace(" ", " \N{NO-BREAK SPACE}")) 227 228 @renderer 229 def lineNumber(self, request, tag): 230 """ 231 Render the line number as a child of C{tag}. 232 """ 233 return tag(str(self.number)) 234 235 236class _SourceFragmentElement(Element): 237 """ 238 L{_SourceFragmentElement} is an L{IRenderable} which can render several lines 239 of source code near the line number of a particular frame object. 240 241 @ivar frame: A L{Failure<twisted.python.failure.Failure>}-style frame object 242 for which to load a source line to render. This is really a tuple 243 holding some information from a frame object. See 244 L{Failure.frames<twisted.python.failure.Failure>} for specifics. 245 """ 246 247 def __init__(self, loader, frame): 248 Element.__init__(self, loader) 249 self.frame = frame 250 251 def _getSourceLines(self): 252 """ 253 Find the source line references by C{self.frame} and yield, in source 254 line order, it and the previous and following lines. 255 256 @return: A generator which yields two-tuples. Each tuple gives a source 257 line number and the contents of that source line. 258 """ 259 filename = self.frame[1] 260 lineNumber = self.frame[2] 261 for snipLineNumber in range(lineNumber - 1, lineNumber + 2): 262 yield (snipLineNumber, linecache.getline(filename, snipLineNumber).rstrip()) 263 264 @renderer 265 def sourceLines(self, request, tag): 266 """ 267 Render the source line indicated by C{self.frame} and several 268 surrounding lines. The active line will be given a I{class} of 269 C{"snippetHighlightLine"}. Other lines will be given a I{class} of 270 C{"snippetLine"}. 271 """ 272 for (lineNumber, sourceLine) in self._getSourceLines(): 273 newTag = tag.clone() 274 if lineNumber == self.frame[2]: 275 cssClass = "snippetHighlightLine" 276 else: 277 cssClass = "snippetLine" 278 loader = TagLoader(newTag(**{"class": cssClass})) 279 yield _SourceLineElement(loader, lineNumber, sourceLine) 280 281 282class _FrameElement(Element): 283 """ 284 L{_FrameElement} is an L{IRenderable} which can render details about one 285 frame from a L{Failure<twisted.python.failure.Failure>}. 286 287 @ivar frame: A L{Failure<twisted.python.failure.Failure>}-style frame object 288 for which to load a source line to render. This is really a tuple 289 holding some information from a frame object. See 290 L{Failure.frames<twisted.python.failure.Failure>} for specifics. 291 """ 292 293 def __init__(self, loader, frame): 294 Element.__init__(self, loader) 295 self.frame = frame 296 297 @renderer 298 def filename(self, request, tag): 299 """ 300 Render the name of the file this frame references as a child of C{tag}. 301 """ 302 return tag(self.frame[1]) 303 304 @renderer 305 def lineNumber(self, request, tag): 306 """ 307 Render the source line number this frame references as a child of 308 C{tag}. 309 """ 310 return tag(str(self.frame[2])) 311 312 @renderer 313 def function(self, request, tag): 314 """ 315 Render the function name this frame references as a child of C{tag}. 316 """ 317 return tag(self.frame[0]) 318 319 @renderer 320 def source(self, request, tag): 321 """ 322 Render the source code surrounding the line this frame references, 323 replacing C{tag}. 324 """ 325 return _SourceFragmentElement(TagLoader(tag), self.frame) 326 327 328class _StackElement(Element): 329 """ 330 L{_StackElement} renders an L{IRenderable} which can render a list of frames. 331 """ 332 333 def __init__(self, loader, stackFrames): 334 Element.__init__(self, loader) 335 self.stackFrames = stackFrames 336 337 @renderer 338 def frames(self, request, tag): 339 """ 340 Render the list of frames in this L{_StackElement}, replacing C{tag}. 341 """ 342 return [ 343 _FrameElement(TagLoader(tag.clone()), frame) for frame in self.stackFrames 344 ] 345 346 347class _NSContext: 348 """ 349 A mapping from XML namespaces onto their prefixes in the document. 350 """ 351 352 def __init__(self, parent: Optional["_NSContext"] = None): 353 """ 354 Pull out the parent's namespaces, if there's no parent then default to 355 XML. 356 """ 357 self.parent = parent 358 if parent is not None: 359 self.nss: Dict[Optional[str], Optional[str]] = OrderedDict(parent.nss) 360 else: 361 self.nss = {"http://www.w3.org/XML/1998/namespace": "xml"} 362 363 def get(self, k: Optional[str], d: Optional[str] = None) -> Optional[str]: 364 """ 365 Get a prefix for a namespace. 366 367 @param d: The default prefix value. 368 """ 369 return self.nss.get(k, d) 370 371 def __setitem__(self, k: Optional[str], v: Optional[str]) -> None: 372 """ 373 Proxy through to setting the prefix for the namespace. 374 """ 375 self.nss.__setitem__(k, v) 376 377 def __getitem__(self, k: Optional[str]) -> Optional[str]: 378 """ 379 Proxy through to getting the prefix for the namespace. 380 """ 381 return self.nss.__getitem__(k) 382 383 384TEMPLATE_NAMESPACE = "http://twistedmatrix.com/ns/twisted.web.template/0.1" 385 386 387class _ToStan(handler.ContentHandler, handler.EntityResolver): 388 """ 389 A SAX parser which converts an XML document to the Twisted STAN 390 Document Object Model. 391 """ 392 393 def __init__(self, sourceFilename: Optional[str]): 394 """ 395 @param sourceFilename: the filename the XML was loaded out of. 396 """ 397 self.sourceFilename = sourceFilename 398 self.prefixMap = _NSContext() 399 self.inCDATA = False 400 401 def setDocumentLocator(self, locator: Locator) -> None: 402 """ 403 Set the document locator, which knows about line and character numbers. 404 """ 405 self.locator = locator 406 407 def startDocument(self) -> None: 408 """ 409 Initialise the document. 410 """ 411 # Depending on our active context, the element type can be Tag, slot 412 # or str. Since mypy doesn't understand that context, it would be 413 # a pain to not use Any here. 414 self.document: List[Any] = [] 415 self.current = self.document 416 self.stack: List[Any] = [] 417 self.xmlnsAttrs: List[Tuple[str, str]] = [] 418 419 def endDocument(self) -> None: 420 """ 421 Document ended. 422 """ 423 424 def processingInstruction(self, target: str, data: str) -> None: 425 """ 426 Processing instructions are ignored. 427 """ 428 429 def startPrefixMapping(self, prefix: Optional[str], uri: str) -> None: 430 """ 431 Set up the prefix mapping, which maps fully qualified namespace URIs 432 onto namespace prefixes. 433 434 This gets called before startElementNS whenever an C{xmlns} attribute 435 is seen. 436 """ 437 438 self.prefixMap = _NSContext(self.prefixMap) 439 self.prefixMap[uri] = prefix 440 441 # Ignore the template namespace; we'll replace those during parsing. 442 if uri == TEMPLATE_NAMESPACE: 443 return 444 445 # Add to a list that will be applied once we have the element. 446 if prefix is None: 447 self.xmlnsAttrs.append(("xmlns", uri)) 448 else: 449 self.xmlnsAttrs.append(("xmlns:%s" % prefix, uri)) 450 451 def endPrefixMapping(self, prefix: Optional[str]) -> None: 452 """ 453 "Pops the stack" on the prefix mapping. 454 455 Gets called after endElementNS. 456 """ 457 parent = self.prefixMap.parent 458 assert parent is not None, "More prefix mapping ends than starts" 459 self.prefixMap = parent 460 461 def startElementNS( 462 self, 463 namespaceAndName: Tuple[str, str], 464 qname: Optional[str], 465 attrs: Mapping[Tuple[Optional[str], str], str], 466 ) -> None: 467 """ 468 Gets called when we encounter a new xmlns attribute. 469 470 @param namespaceAndName: a (namespace, name) tuple, where name 471 determines which type of action to take, if the namespace matches 472 L{TEMPLATE_NAMESPACE}. 473 @param qname: ignored. 474 @param attrs: attributes on the element being started. 475 """ 476 477 filename = self.sourceFilename 478 lineNumber = self.locator.getLineNumber() 479 columnNumber = self.locator.getColumnNumber() 480 481 ns, name = namespaceAndName 482 if ns == TEMPLATE_NAMESPACE: 483 if name == "transparent": 484 name = "" 485 elif name == "slot": 486 default: Optional[str] 487 try: 488 # Try to get the default value for the slot 489 default = attrs[(None, "default")] 490 except KeyError: 491 # If there wasn't one, then use None to indicate no 492 # default. 493 default = None 494 sl = slot( 495 attrs[(None, "name")], 496 default=default, 497 filename=filename, 498 lineNumber=lineNumber, 499 columnNumber=columnNumber, 500 ) 501 self.stack.append(sl) 502 self.current.append(sl) 503 self.current = sl.children 504 return 505 506 render = None 507 508 attrs = OrderedDict(attrs) 509 for k, v in list(attrs.items()): 510 attrNS, justTheName = k 511 if attrNS != TEMPLATE_NAMESPACE: 512 continue 513 if justTheName == "render": 514 render = v 515 del attrs[k] 516 517 # nonTemplateAttrs is a dictionary mapping attributes that are *not* in 518 # TEMPLATE_NAMESPACE to their values. Those in TEMPLATE_NAMESPACE were 519 # just removed from 'attrs' in the loop immediately above. The key in 520 # nonTemplateAttrs is either simply the attribute name (if it was not 521 # specified as having a namespace in the template) or prefix:name, 522 # preserving the xml namespace prefix given in the document. 523 524 nonTemplateAttrs = OrderedDict() 525 for (attrNs, attrName), v in attrs.items(): 526 nsPrefix = self.prefixMap.get(attrNs) 527 if nsPrefix is None: 528 attrKey = attrName 529 else: 530 attrKey = f"{nsPrefix}:{attrName}" 531 nonTemplateAttrs[attrKey] = v 532 533 if ns == TEMPLATE_NAMESPACE and name == "attr": 534 if not self.stack: 535 # TODO: define a better exception for this? 536 raise AssertionError( 537 f"<{{{TEMPLATE_NAMESPACE}}}attr> as top-level element" 538 ) 539 if "name" not in nonTemplateAttrs: 540 # TODO: same here 541 raise AssertionError( 542 f"<{{{TEMPLATE_NAMESPACE}}}attr> requires a name attribute" 543 ) 544 el = Tag( 545 "", 546 render=render, 547 filename=filename, 548 lineNumber=lineNumber, 549 columnNumber=columnNumber, 550 ) 551 self.stack[-1].attributes[nonTemplateAttrs["name"]] = el 552 self.stack.append(el) 553 self.current = el.children 554 return 555 556 # Apply any xmlns attributes 557 if self.xmlnsAttrs: 558 nonTemplateAttrs.update(OrderedDict(self.xmlnsAttrs)) 559 self.xmlnsAttrs = [] 560 561 # Add the prefix that was used in the parsed template for non-template 562 # namespaces (which will not be consumed anyway). 563 if ns != TEMPLATE_NAMESPACE and ns is not None: 564 prefix = self.prefixMap[ns] 565 if prefix is not None: 566 name = f"{self.prefixMap[ns]}:{name}" 567 el = Tag( 568 name, 569 attributes=OrderedDict( 570 cast(Mapping[Union[bytes, str], str], nonTemplateAttrs) 571 ), 572 render=render, 573 filename=filename, 574 lineNumber=lineNumber, 575 columnNumber=columnNumber, 576 ) 577 self.stack.append(el) 578 self.current.append(el) 579 self.current = el.children 580 581 def characters(self, ch: str) -> None: 582 """ 583 Called when we receive some characters. CDATA characters get passed 584 through as is. 585 """ 586 if self.inCDATA: 587 self.stack[-1].append(ch) 588 return 589 self.current.append(ch) 590 591 def endElementNS(self, name: Tuple[str, str], qname: Optional[str]) -> None: 592 """ 593 A namespace tag is closed. Pop the stack, if there's anything left in 594 it, otherwise return to the document's namespace. 595 """ 596 self.stack.pop() 597 if self.stack: 598 self.current = self.stack[-1].children 599 else: 600 self.current = self.document 601 602 def startDTD(self, name: str, publicId: str, systemId: str) -> None: 603 """ 604 DTDs are ignored. 605 """ 606 607 def endDTD(self, *args: object) -> None: 608 """ 609 DTDs are ignored. 610 """ 611 612 def startCDATA(self) -> None: 613 """ 614 We're starting to be in a CDATA element, make a note of this. 615 """ 616 self.inCDATA = True 617 self.stack.append([]) 618 619 def endCDATA(self) -> None: 620 """ 621 We're no longer in a CDATA element. Collect up the characters we've 622 parsed and put them in a new CDATA object. 623 """ 624 self.inCDATA = False 625 comment = "".join(self.stack.pop()) 626 self.current.append(CDATA(comment)) 627 628 def comment(self, content: str) -> None: 629 """ 630 Add an XML comment which we've encountered. 631 """ 632 self.current.append(Comment(content)) 633 634 635def _flatsaxParse(fl: Union[IO[AnyStr], str]) -> List["Flattenable"]: 636 """ 637 Perform a SAX parse of an XML document with the _ToStan class. 638 639 @param fl: The XML document to be parsed. 640 641 @return: a C{list} of Stan objects. 642 """ 643 parser = make_parser() 644 parser.setFeature(handler.feature_validation, 0) 645 parser.setFeature(handler.feature_namespaces, 1) 646 parser.setFeature(handler.feature_external_ges, 0) 647 parser.setFeature(handler.feature_external_pes, 0) 648 649 s = _ToStan(getattr(fl, "name", None)) 650 parser.setContentHandler(s) 651 parser.setEntityResolver(s) 652 parser.setProperty(handler.property_lexical_handler, s) 653 654 parser.parse(fl) 655 656 return s.document 657 658 659@implementer(ITemplateLoader) 660class XMLString: 661 """ 662 An L{ITemplateLoader} that loads and parses XML from a string. 663 """ 664 665 def __init__(self, s: Union[str, bytes]): 666 """ 667 Run the parser on a L{io.StringIO} copy of the string. 668 669 @param s: The string from which to load the XML. 670 @type s: L{str}, or a UTF-8 encoded L{bytes}. 671 """ 672 if not isinstance(s, str): 673 s = s.decode("utf8") 674 675 self._loadedTemplate: List["Flattenable"] = _flatsaxParse(io.StringIO(s)) 676 """The loaded document.""" 677 678 def load(self) -> List["Flattenable"]: 679 """ 680 Return the document. 681 682 @return: the loaded document. 683 """ 684 return self._loadedTemplate 685 686 687class FailureElement(Element): 688 """ 689 L{FailureElement} is an L{IRenderable} which can render detailed information 690 about a L{Failure<twisted.python.failure.Failure>}. 691 692 @ivar failure: The L{Failure<twisted.python.failure.Failure>} instance which 693 will be rendered. 694 695 @since: 12.1 696 """ 697 698 loader = XMLString( 699 """ 700<div xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1"> 701 <style type="text/css"> 702 div.error { 703 color: red; 704 font-family: Verdana, Arial, helvetica, sans-serif; 705 font-weight: bold; 706 } 707 708 div { 709 font-family: Verdana, Arial, helvetica, sans-serif; 710 } 711 712 div.stackTrace { 713 } 714 715 div.frame { 716 padding: 1em; 717 background: white; 718 border-bottom: thin black dashed; 719 } 720 721 div.frame:first-child { 722 padding: 1em; 723 background: white; 724 border-top: thin black dashed; 725 border-bottom: thin black dashed; 726 } 727 728 div.location { 729 } 730 731 span.function { 732 font-weight: bold; 733 font-family: "Courier New", courier, monospace; 734 } 735 736 div.snippet { 737 margin-bottom: 0.5em; 738 margin-left: 1em; 739 background: #FFFFDD; 740 } 741 742 div.snippetHighlightLine { 743 color: red; 744 } 745 746 span.code { 747 font-family: "Courier New", courier, monospace; 748 } 749 </style> 750 751 <div class="error"> 752 <span t:render="type" />: <span t:render="value" /> 753 </div> 754 <div class="stackTrace" t:render="traceback"> 755 <div class="frame" t:render="frames"> 756 <div class="location"> 757 <span t:render="filename" />:<span t:render="lineNumber" /> in 758 <span class="function" t:render="function" /> 759 </div> 760 <div class="snippet" t:render="source"> 761 <div t:render="sourceLines"> 762 <span class="lineno" t:render="lineNumber" /> 763 <code class="code" t:render="sourceLine" /> 764 </div> 765 </div> 766 </div> 767 </div> 768 <div class="error"> 769 <span t:render="type" />: <span t:render="value" /> 770 </div> 771</div> 772""" 773 ) 774 775 def __init__(self, failure, loader=None): 776 Element.__init__(self, loader) 777 self.failure = failure 778 779 @renderer 780 def type(self, request, tag): 781 """ 782 Render the exception type as a child of C{tag}. 783 """ 784 return tag(fullyQualifiedName(self.failure.type)) 785 786 @renderer 787 def value(self, request, tag): 788 """ 789 Render the exception value as a child of C{tag}. 790 """ 791 return tag(str(self.failure.value).encode("utf8")) 792 793 @renderer 794 def traceback(self, request, tag): 795 """ 796 Render all the frames in the wrapped 797 L{Failure<twisted.python.failure.Failure>}'s traceback stack, replacing 798 C{tag}. 799 """ 800 return _StackElement(TagLoader(tag), self.failure.frames) 801 802 803def formatFailure(myFailure): 804 """ 805 Construct an HTML representation of the given failure. 806 807 Consider using L{FailureElement} instead. 808 809 @type myFailure: L{Failure<twisted.python.failure.Failure>} 810 811 @rtype: L{bytes} 812 @return: A string containing the HTML representation of the given failure. 813 """ 814 result = [] 815 flattenString(None, FailureElement(myFailure)).addBoth(result.append) 816 if isinstance(result[0], bytes): 817 # Ensure the result string is all ASCII, for compatibility with the 818 # default encoding expected by browsers. 819 return result[0].decode("utf-8").encode("ascii", "xmlcharrefreplace") 820 result[0].raiseException() 821 822 823# Go read the definition of NOT_DONE_YET. For lulz. This is totally 824# equivalent. And this turns out to be necessary, because trying to import 825# NOT_DONE_YET in this module causes a circular import which we cannot escape 826# from. From which we cannot escape. Etc. glyph is okay with this solution for 827# now, and so am I, as long as this comment stays to explain to future 828# maintainers what it means. ~ C. 829# 830# See http://twistedmatrix.com/trac/ticket/5557 for progress on fixing this. 831NOT_DONE_YET = 1 832_moduleLog = Logger() 833 834 835@implementer(ITemplateLoader) 836class TagLoader: 837 """ 838 An L{ITemplateLoader} that loads an existing flattenable object. 839 """ 840 841 def __init__(self, tag: "Flattenable"): 842 """ 843 @param tag: The object which will be loaded. 844 """ 845 846 self.tag: "Flattenable" = tag 847 """The object which will be loaded.""" 848 849 def load(self) -> List["Flattenable"]: 850 return [self.tag] 851 852 853@implementer(ITemplateLoader) 854class XMLFile: 855 """ 856 An L{ITemplateLoader} that loads and parses XML from a file. 857 """ 858 859 def __init__(self, path: FilePath): 860 """ 861 Run the parser on a file. 862 863 @param path: The file from which to load the XML. 864 """ 865 if not isinstance(path, FilePath): 866 warnings.warn( # type: ignore[unreachable] 867 "Passing filenames or file objects to XMLFile is deprecated " 868 "since Twisted 12.1. Pass a FilePath instead.", 869 category=DeprecationWarning, 870 stacklevel=2, 871 ) 872 873 self._loadedTemplate: Optional[List["Flattenable"]] = None 874 """The loaded document, or L{None}, if not loaded.""" 875 876 self._path: FilePath = path 877 """The file that is being loaded from.""" 878 879 def _loadDoc(self) -> List["Flattenable"]: 880 """ 881 Read and parse the XML. 882 883 @return: the loaded document. 884 """ 885 if not isinstance(self._path, FilePath): 886 return _flatsaxParse(self._path) # type: ignore[unreachable] 887 else: 888 with self._path.open("r") as f: 889 return _flatsaxParse(f) 890 891 def __repr__(self) -> str: 892 return f"<XMLFile of {self._path!r}>" 893 894 def load(self) -> List["Flattenable"]: 895 """ 896 Return the document, first loading it if necessary. 897 898 @return: the loaded document. 899 """ 900 if self._loadedTemplate is None: 901 self._loadedTemplate = self._loadDoc() 902 return self._loadedTemplate 903 904 905# Last updated October 2011, using W3Schools as a reference. Link: 906# http://www.w3schools.com/html5/html5_reference.asp 907# Note that <xmp> is explicitly omitted; its semantics do not work with 908# t.w.template and it is officially deprecated. 909VALID_HTML_TAG_NAMES = { 910 "a", 911 "abbr", 912 "acronym", 913 "address", 914 "applet", 915 "area", 916 "article", 917 "aside", 918 "audio", 919 "b", 920 "base", 921 "basefont", 922 "bdi", 923 "bdo", 924 "big", 925 "blockquote", 926 "body", 927 "br", 928 "button", 929 "canvas", 930 "caption", 931 "center", 932 "cite", 933 "code", 934 "col", 935 "colgroup", 936 "command", 937 "datalist", 938 "dd", 939 "del", 940 "details", 941 "dfn", 942 "dir", 943 "div", 944 "dl", 945 "dt", 946 "em", 947 "embed", 948 "fieldset", 949 "figcaption", 950 "figure", 951 "font", 952 "footer", 953 "form", 954 "frame", 955 "frameset", 956 "h1", 957 "h2", 958 "h3", 959 "h4", 960 "h5", 961 "h6", 962 "head", 963 "header", 964 "hgroup", 965 "hr", 966 "html", 967 "i", 968 "iframe", 969 "img", 970 "input", 971 "ins", 972 "isindex", 973 "keygen", 974 "kbd", 975 "label", 976 "legend", 977 "li", 978 "link", 979 "map", 980 "mark", 981 "menu", 982 "meta", 983 "meter", 984 "nav", 985 "noframes", 986 "noscript", 987 "object", 988 "ol", 989 "optgroup", 990 "option", 991 "output", 992 "p", 993 "param", 994 "pre", 995 "progress", 996 "q", 997 "rp", 998 "rt", 999 "ruby", 1000 "s", 1001 "samp", 1002 "script", 1003 "section", 1004 "select", 1005 "small", 1006 "source", 1007 "span", 1008 "strike", 1009 "strong", 1010 "style", 1011 "sub", 1012 "summary", 1013 "sup", 1014 "table", 1015 "tbody", 1016 "td", 1017 "textarea", 1018 "tfoot", 1019 "th", 1020 "thead", 1021 "time", 1022 "title", 1023 "tr", 1024 "tt", 1025 "u", 1026 "ul", 1027 "var", 1028 "video", 1029 "wbr", 1030} 1031 1032 1033class _TagFactory: 1034 """ 1035 A factory for L{Tag} objects; the implementation of the L{tags} object. 1036 1037 This allows for the syntactic convenience of C{from twisted.web.html import 1038 tags; tags.a(href="linked-page.html")}, where 'a' can be basically any HTML 1039 tag. 1040 1041 The class is not exposed publicly because you only ever need one of these, 1042 and we already made it for you. 1043 1044 @see: L{tags} 1045 """ 1046 1047 def __getattr__(self, tagName: str) -> Tag: 1048 if tagName == "transparent": 1049 return Tag("") 1050 # allow for E.del as E.del_ 1051 tagName = tagName.rstrip("_") 1052 if tagName not in VALID_HTML_TAG_NAMES: 1053 raise AttributeError(f"unknown tag {tagName!r}") 1054 return Tag(tagName) 1055 1056 1057tags = _TagFactory() 1058 1059 1060def renderElement( 1061 request: IRequest, 1062 element: IRenderable, 1063 doctype: Optional[bytes] = b"<!DOCTYPE html>", 1064 _failElement: Optional[Callable[[Failure], "Element"]] = None, 1065) -> object: 1066 """ 1067 Render an element or other L{IRenderable}. 1068 1069 @param request: The L{IRequest} being rendered to. 1070 @param element: An L{IRenderable} which will be rendered. 1071 @param doctype: A L{bytes} which will be written as the first line of 1072 the request, or L{None} to disable writing of a doctype. The argument 1073 should not include a trailing newline and will default to the HTML5 1074 doctype C{'<!DOCTYPE html>'}. 1075 1076 @returns: NOT_DONE_YET 1077 1078 @since: 12.1 1079 """ 1080 if doctype is not None: 1081 request.write(doctype) 1082 request.write(b"\n") 1083 1084 if _failElement is None: 1085 _failElement = FailureElement 1086 1087 d = flatten(request, element, request.write) 1088 1089 def eb(failure: Failure) -> Optional[Deferred[None]]: 1090 _moduleLog.failure( 1091 "An error occurred while rendering the response.", failure=failure 1092 ) 1093 site = getattr(request, "site", None) 1094 if site is not None and site.displayTracebacks: 1095 assert _failElement is not None 1096 return flatten(request, _failElement(failure), request.write) 1097 else: 1098 request.write( 1099 b'<div style="font-size:800%;' 1100 b"background-color:#FFF;" 1101 b"color:#F00" 1102 b'">An error occurred while rendering the response.</div>' 1103 ) 1104 return None 1105 1106 def finish(result: object, *, request: IRequest = request) -> object: 1107 request.finish() 1108 return result 1109 1110 d.addErrback(eb) 1111 d.addBoth(finish) 1112 return NOT_DONE_YET 1113