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