|
1 import os |
|
2 import urllib |
|
3 import time |
|
4 import re |
|
5 from cPickle import load, dump |
|
6 from webob import Request, Response, html_escape |
|
7 from webob import exc |
|
8 |
|
9 class 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_int != 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 |
|
118 if __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 |
|
149 |
|
150 |