Source code for pants.web.utils

###############################################################################
#
# Copyright 2011-2012 Pants Developers (see AUTHORS.txt)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
###############################################################################

###############################################################################
# Imports
###############################################################################

import base64
import logging
import re
import string
import sys

from pants.http.utils import HTTP, SERVER, SERVER_URL


try:
    from pkg_resources import resource_string
except ImportError:
    def resource_string(*args):
        raise IOError("pkg_resources not available.")

###############################################################################
# Logging
###############################################################################

log = logging.getLogger("pants.web")

###############################################################################
# Constants
###############################################################################

HAIKUS = {
    400: u'Something you entered<br>'
         u'transcended parameters.<br>'
         u'So much is unknown.',

    401: u'To access this page,<br>'
         u'one must know oneself; but then:<br>'
         u'inform the server.',

    403: u'Unfortunately,<br>'
         u'permissions insufficient.<br>'
         u'This, you cannot see.',

    404: u'You step in the stream,<br>'
         u'But the water has moved on.<br>'
         u'This page is not here.',

    410: u'A file that big?<br>'
         u'It might be very useful.<br>'
         u'But now it is Gone.',

    413: u'Out of memory.<br>'
         u'We wish to hold the whole sky,<br>'
         u'But we never will.',

    418: u'You requested coffee,<br>'
         u'it is neither short nor stout.<br>'
         u'I am a teapot.',

    500: u'Chaos reigns within.<br>'
         u'Reflect, repent, and reboot.<br>'
         u'Order shall return.'
}

if sys.platform.startswith('win'):
    HAIKUS[500] = (u'Yesterday it worked.<br>'
        u'Today, it is not working.<br>'
        u'Windows is like that.')

HTTP_MESSAGES = {
    401: u'You must sign in to access this page.',
    403: u'You do not have permission to view this page.',
    404: u'The page at <code>{url}</code> cannot be found.',
    500: u'The server encountered an internal error and cannot display '
         u'this page.'
}

# Formats for parse_date to use.
DATE_FORMATS = (
    "%a, %d %b %Y %H:%M:%S %Z",
    "%A, %d-%b-%y %H:%M:%S %Z",
    "%a %b %d %H:%M:%S %Y",
    )

# HTTP status codes that should not send response bodies.
NO_BODY_CODES = (204, 205, 304)


###############################################################################
# Resources
###############################################################################

# The Console JS
try:
    CONSOLE_JS = resource_string("pants.web", "data/console.js")
except IOError:
    # This message is commented out because the console JS isn't actually
    # *used* yet in the code, and can be safely ignored.
    # log.debug("Unable to load pants.web console JS from %r." % DATA_DIR)
    CONSOLE_JS = ""

# The Main CSS
try:
    MAIN_CSS = resource_string("pants.web", "data/main.css")
except IOError:
    log.debug("Unable to load pants.web main CSS.")
    MAIN_CSS = ""

# The Directory CSS
try:
    DIRECTORY_CSS = resource_string("pants.web", "data/directory.css")
except IOError:
    log.debug("Unable to load pants.web directory CSS.")
    DIRECTORY_CSS = ""

# The Images
IMAGES = {}
for name in ('audio', 'document', 'folder', 'image', 'video', 'zip'):
    try:
        IMAGES[name] = base64.b64encode(resource_string("pants.web",
                                                        "data/%s.png" % name))
    except IOError:
        log.debug("Unable to load pants.web icon %r." % name)

# Insert the images.
DIRECTORY_CSS = string.Template(DIRECTORY_CSS).safe_substitute(**IMAGES)

# The Main Template
try:
    PAGE = resource_string("pants.web", "data/main.html")
except IOError:
    log.debug("Unable to load pants.web page template.")
    PAGE = u"""<!DOCTYPE html>
<title>$title</title>
$content
<hr>
<address><a href="$server_url">$server</a> at
<a href="$scheme://$host">$scheme://$host</a></address>"""

# Fill up the template a bit.
PAGE = string.Template(PAGE).safe_substitute(
                                css=MAIN_CSS,
                                server_url=SERVER_URL,
                                server=SERVER)

PAGE = re.compile(">\s+<", flags=re.DOTALL).sub("><", PAGE)
PAGE = string.Template(PAGE)

# The Directory Template
try:
    DIRECTORY_PAGE = PAGE.safe_substitute(
                            title="Index of $path",
                            content=resource_string("pants.web",
                                                    "data/directory.html"),
                            extra_css=DIRECTORY_CSS
                            )
except IOError:
    DIRECTORY_PAGE = PAGE.safe_substitute(
                            title="Index of $path",
                            content="""<h1>Index of $path</h1>
$go_up
<table><thead><tr><th>Name</th><th>Size</th><th>Last Modified</th></tr></thead>
$content
</table>""",
                            extra_css=DIRECTORY_CSS
                            )

DIRECTORY_PAGE = string.Template(DIRECTORY_PAGE)

# Directory Entry Template
try:
    DIRECTORY_ENTRY = resource_string("pants.web", "data/entry.html")
except IOError:
    DIRECTORY_ENTRY = '<tr><td><a class="icon $cls" href="$url">$name</a>' + \
                      '</td><td>$size</td><td>$modified</td></tr>'

DIRECTORY_ENTRY = string.Template(DIRECTORY_ENTRY)

# The Error Template
try:
    ERROR_PAGE = PAGE.safe_substitute(
                        title="$status $status_text",
                        content=resource_string("pants.web", "data/error.html"),
                        extra_css=u'')
except IOError:
    log.warning("Unable to load pants.web error template.")
    ERROR_PAGE = PAGE.safe_substitute(
                        title="$status $status_text",
                        extra_css=u'',
                        content=u"""<h1>$status $status_text</h1>
$content""")

ERROR_PAGE = string.Template(ERROR_PAGE)


###############################################################################
# Special Exceptions
###############################################################################

[docs]class HTTPException(Exception): """ Raising an instance of HTTPException will cause the Application to render an error page out to the client with the given `HTTP status code <http://en.wikipedia.org/wiki/List_of_HTTP_status_codes>`_, message, and any provided headers. This is, generally, preferable to allowing an exception of a different type to bubble up to the Application, which would result in a ``500 Internal Server Error`` page. The :func:`abort` helper function makes it easy to raise instances of this exception. ========= ============ Argument Description ========= ============ status *Optional.* The `HTTP status code <http://en.wikipedia.org/wiki/List_of_HTTP_status_codes>`_ to generate an error page for. If this isn't specified, a ``404 Not Found`` page will be generated. message *Optional.* A text message to display on the error page. headers *Optional.* A dict of extra HTTP headers to return with the rendered page. ========= ============ """ def __init__(self, status=404, message=None, headers=None): super(HTTPException, self).__init__(status, message, headers) def __str__(self): return "%d %s [message=%r, headers=%r]" % \ (self.status, HTTP.get(self.status, ''), self.args[1], self.args[2]) def __repr__(self): return "HTTPException(status=%r, message=%r, headers=%r)" % self.args @property def status(self): return self.args[0] @property def message(self): return self.args[1] @property def headers(self): return self.args[2]
[docs]class HTTPTransparentRedirect(Exception): """ Raising an instance of HTTPTransparentRedirect will cause the Application to silently redirect a request to a new URL. """ def __init__(self, url): super(HTTPTransparentRedirect, self).__init__(url) def __str__(self): return "url=%r" % self.args[0] def __repr__(self): return "%s(url=%r)" % (self.__class__.__name__, self.args[0]) @property def url(self): return self.args[0]
############################################################################### # Private Helper Functions ############################################################################### _encodings = ('utf-8','iso-8859-1','cp1252','latin1') def decode(text): for enc in _encodings: try: return text.decode(enc) except UnicodeDecodeError: continue else: return text.decode('utf-8','ignore')