1import os
2import urllib
3import time
4import re
5from cPickle import load, dump
6from webob import Request, Response, html_escape
7from webob import exc
8
9class Commenter(object):
10
11    def __init__(self, app, storage_dir):
12        self.app = app
13        self.storage_dir = storage_dir
14        if not os.path.exists(storage_dir):
15            os.makedirs(storage_dir)
16
17    def __call__(self, environ, start_response):
18        req = Request(environ)
19        if req.path_info_peek() == '.comments':
20            return self.process_comment(req)(environ, start_response)
21        # This is the base path of *this* middleware:
22        base_url = req.application_url
23        resp = req.get_response(self.app)
24        if resp.content_type != 'text/html' or resp.status_code != 200:
25            # Not an HTML response, we don't want to
26            # do anything to it
27            return resp(environ, start_response)
28        # Make sure the content isn't gzipped:
29        resp.decode_content()
30        comments = self.get_data(req.url)
31        body = resp.body
32        body = self.add_to_end(body, self.format_comments(comments))
33        body = self.add_to_end(body, self.submit_form(base_url, req))
34        resp.body = body
35        return resp(environ, start_response)
36
37    def get_data(self, url):
38        # Double-quoting makes the filename safe
39        filename = self.url_filename(url)
40        if not os.path.exists(filename):
41            return []
42        else:
43            f = open(filename, 'rb')
44            data = load(f)
45            f.close()
46            return data
47
48    def save_data(self, url, data):
49        filename = self.url_filename(url)
50        f = open(filename, 'wb')
51        dump(data, f)
52        f.close()
53
54    def url_filename(self, url):
55        return os.path.join(self.storage_dir, urllib.quote(url, ''))
56
57    _end_body_re = re.compile(r'</body.*?>', re.I|re.S)
58
59    def add_to_end(self, html, extra_html):
60        """
61        Adds extra_html to the end of the html page (before </body>)
62        """
63        match = self._end_body_re.search(html)
64        if not match:
65            return html + extra_html
66        else:
67            return html[:match.start()] + extra_html + html[match.start():]
68
69    def format_comments(self, comments):
70        if not comments:
71            return ''
72        text = []
73        text.append('<hr>')
74        text.append('<h2><a name="comment-area"></a>Comments (%s):</h2>' % len(comments))
75        for comment in comments:
76            text.append('<h3><a href="%s">%s</a> at %s:</h3>' % (
77                html_escape(comment['homepage']), html_escape(comment['name']),
78                time.strftime('%c', comment['time'])))
79            # Susceptible to XSS attacks!:
80            text.append(comment['comments'])
81        return ''.join(text)
82
83    def submit_form(self, base_path, req):
84        return '''<h2>Leave a comment:</h2>
85        <form action="%s/.comments" method="POST">
86         <input type="hidden" name="url" value="%s">
87         <table width="100%%">
88          <tr><td>Name:</td>
89              <td><input type="text" name="name" style="width: 100%%"></td></tr>
90          <tr><td>URL:</td>
91              <td><input type="text" name="homepage" style="width: 100%%"></td></tr>
92         </table>
93         Comments:<br>
94         <textarea name="comments" rows=10 style="width: 100%%"></textarea><br>
95         <input type="submit" value="Submit comment">
96        </form>
97        ''' % (base_path, html_escape(req.url))
98
99    def process_comment(self, req):
100        try:
101            url = req.params['url']
102            name = req.params['name']
103            homepage = req.params['homepage']
104            comments = req.params['comments']
105        except KeyError, e:
106            resp = exc.HTTPBadRequest('Missing parameter: %s' % e)
107            return resp
108        data = self.get_data(url)
109        data.append(dict(
110            name=name,
111            homepage=homepage,
112            comments=comments,
113            time=time.gmtime()))
114        self.save_data(url, data)
115        resp = exc.HTTPSeeOther(location=url+'#comment-area')
116        return resp
117
118if __name__ == '__main__':
119    import optparse
120    parser = optparse.OptionParser(
121        usage='%prog --port=PORT BASE_DIRECTORY'
122        )
123    parser.add_option(
124        '-p', '--port',
125        default='8080',
126        dest='port',
127        type='int',
128        help='Port to serve on (default 8080)')
129    parser.add_option(
130        '--comment-data',
131        default='./comments',
132        dest='comment_data',
133        help='Place to put comment data into (default ./comments/)')
134    options, args = parser.parse_args()
135    if not args:
136        parser.error('You must give a BASE_DIRECTORY')
137    base_dir = args[0]
138    from paste.urlparser import StaticURLParser
139    app = StaticURLParser(base_dir)
140    app = Commenter(app, options.comment_data)
141    from wsgiref.simple_server import make_server
142    httpd = make_server('localhost', options.port, app)
143    print 'Serving on http://localhost:%s' % options.port
144    try:
145        httpd.serve_forever()
146    except KeyboardInterrupt:
147        print '^C'
148