diff --git a/demos/chat/templates/message.html b/demos/chat/templates/message.html index 4445cbdfaf..20edbe7a63 100644 --- a/demos/chat/templates/message.html +++ b/demos/chat/templates/message.html @@ -1 +1,2 @@ -
{{ escape(message["from"]) }}: {{ escape(message["body"]) }}
+{% import tornado.escape %} +
{{ escape(message["from"]) }}: {{ tornado.escape.linkify(message["body"]) }}
diff --git a/tornado/escape.py b/tornado/escape.py index 5d6d9ea780..174c71cea3 100644 --- a/tornado/escape.py +++ b/tornado/escape.py @@ -49,7 +49,7 @@ def _json_decode(s): def xhtml_escape(value): """Escapes a string so it is valid within XML or XHTML.""" - return utf8(xml.sax.saxutils.escape(value, {'"': """})) + return xml.sax.saxutils.escape(value, {'"': """}) def xhtml_unescape(value): @@ -95,6 +95,90 @@ def utf8(value): return value +# Regex from http://daringfireball.net/2010/07/improved_regex_for_matching_urls +# Modified to capture protocol and to avoid HTML character entities other than & +_URL_RE = re.compile(ur"""(?i)\b((?:([a-z][\w-]+):(?:(/{1,3})|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>&]+|&|\(([^\s()<>&]+|(\([^\s()<>&]+\)))*\))+(?:\(([^\s()<>&]+|(\([^\s()<>&]+\)))*\)|[^\s`!()\[\]{};:'".,<>?\xab\xbb\u201c\u201d\u2018\u2019&]))""") + + +def linkify(text, shorten=False, extra_params="", + require_protocol=False, permitted_protocols=["http", "https"]): + """Converts plain text into HTML with links. + + For example: linkify("Hello http://tornadoweb.org!") would return + Hello http://tornadoweb.org! + + Parameters: + shorten: Long urls will be shortened for display. + extra_params: Extra text to include in the link tag, + e.g. linkify(text, extra_params='rel="nofollow" class="external"') + require_protocol: Only linkify urls which include a protocol. If this is + False, urls such as www.facebook.com will also be linkified. + permitted_protocols: List (or set) of protocols which should be linkified, + e.g. linkify(text, permitted_protocols=["http", "ftp", "mailto"]). + It is very unsafe to include protocols such as "javascript". + """ + if extra_params: + extra_params = " " + extra_params.strip() + + def make_link(m): + url = m.group(1) + proto = m.group(2) + if require_protocol and not proto: + return url # not protocol, no linkify + + if proto and proto not in permitted_protocols: + return url # bad protocol, no linkify + + href = m.group(1) + if not proto: + href = "http://" + href # no proto specified, use http + + params = extra_params + + # clip long urls. max_len is just an approximation + max_len = 30 + if shorten and len(url) > max_len: + before_clip = url + if proto: + proto_len = len(proto) + 1 + len(m.group(3) or "") # +1 for : + else: + proto_len = 0 + + parts = url[proto_len:].split("/") + if len(parts) > 1: + # Grab the whole host part plus the first bit of the path + # The path is usually not that interesting once shortened + # (no more slug, etc), so it really just provides a little + # extra indication of shortening. + url = url[:proto_len] + parts[0] + "/" + \ + parts[1][:8].split('?')[0].split('.')[0] + + if len(url) > max_len * 1.5: # still too long + url = url[:max_len] + + if url != before_clip: + amp = url.rfind('&') + # avoid splitting html char entities + if amp > max_len - 5: + url = url[:amp] + url += "..." + + if len(url) >= len(before_clip): + url = before_clip + else: + # full url is visible on mouse-over (for those who don't + # have a status bar, such as Safari by default) + params += ' title="%s"' % href + + return u'%s' % (href, params, url) + + # First HTML-escape so that our strings are all safe. + # The regex is modified to avoid character entites other than & so + # that we won't pick up ", etc. + text = _unicode(xhtml_escape(text)) + return _URL_RE.sub(make_link, text) + + def _unicode(value): if isinstance(value, str): return value.decode("utf-8") diff --git a/tornado/iostream.py b/tornado/iostream.py index fe15134af3..c18f8f1d21 100644 --- a/tornado/iostream.py +++ b/tornado/iostream.py @@ -21,6 +21,7 @@ import socket from tornado import ioloop +from tornado import stack_context try: import ssl # Python 2.6+ @@ -79,14 +80,15 @@ def __init__(self, socket, io_loop=None, max_buffer_size=104857600, self._write_callback = None self._close_callback = None self._state = self.io_loop.ERROR - self.io_loop.add_handler( - self.socket.fileno(), self._handle_events, self._state) + with stack_context.NullContext(): + self.io_loop.add_handler( + self.socket.fileno(), self._handle_events, self._state) def read_until(self, delimiter, callback): """Call callback when we read the given delimiter.""" assert not self._read_callback, "Already reading" self._read_delimiter = delimiter - self._read_callback = callback + self._read_callback = stack_context.wrap(callback) while True: # See if we've already got the data from a previous read if self._read_from_buffer(): @@ -103,7 +105,7 @@ def read_bytes(self, num_bytes, callback): callback("") return self._read_bytes = num_bytes - self._read_callback = callback + self._read_callback = stack_context.wrap(callback) while True: if self._read_from_buffer(): return @@ -123,11 +125,11 @@ def write(self, data, callback=None): self._check_closed() self._write_buffer += data self._add_io_state(self.io_loop.WRITE) - self._write_callback = callback + self._write_callback = stack_context.wrap(callback) def set_close_callback(self, callback): """Call the given callback when the stream is closed.""" - self._close_callback = callback + self._close_callback = stack_context.wrap(callback) def close(self): """Close this stream.""" @@ -177,6 +179,8 @@ def _run_callback(self, callback, *args, **kwargs): try: callback(*args, **kwargs) except: + logging.error("Uncaught exception, closing connection.", + exc_info=True) # Close the socket on an uncaught exception from a user callback # (It would eventually get closed when the socket object is # gc'd, but we don't want to rely on gc happening before we diff --git a/tornado/stack_context.py b/tornado/stack_context.py index 9a835e7c78..ae95e005cc 100644 --- a/tornado/stack_context.py +++ b/tornado/stack_context.py @@ -101,6 +101,8 @@ def wrap(fn): different execution context (either in a different thread or asynchronously in the same thread). ''' + if fn is None: + return None # functools.wraps doesn't appear to work on functools.partial objects #@functools.wraps(fn) def wrapped(callback, contexts, *args, **kwargs): diff --git a/website/templates/index.html b/website/templates/index.html index 0ec87e37dc..a4e647acf7 100644 --- a/website/templates/index.html +++ b/website/templates/index.html @@ -45,18 +45,6 @@

Hello, world

See the Tornado documentation for a detailed walkthrough of the framework.

Discussion and support

-

You can discuss Tornado and report bugs on the Tornado developer mailing list. - -

Links and resources

- - -

Updates

-

Follow us on Facebook, Twitter, or FriendFeed to get updates and announcements:

-
FacebookTwitterFacebook
+

You can discuss Tornado and report bugs on the Tornado developer mailing list. Links to additional resources can be found on the Tornado wiki. {% end %}