1import os
2import re
3from webob import Request, Response
4from webob import exc
5from tempita import HTMLTemplate
6
7VIEW_TEMPLATE = HTMLTemplate("""\
8<html>
9 <head>
10  <title>{{page.title}}</title>
11 </head>
12 <body>
13<h1>{{page.title}}</h1>
14{{if message}}
15<div style="background-color: #99f">{{message}}</div>
16{{endif}}
17
18<div>{{page.content|html}}</div>
19
20<hr>
21<a href="{{req.url}}?action=edit">Edit</a>
22 </body>
23</html>
24""")
25
26EDIT_TEMPLATE = HTMLTemplate("""\
27<html>
28 <head>
29  <title>Edit: {{page.title}}</title>
30 </head>
31 <body>
32{{if page.exists}}
33<h1>Edit: {{page.title}}</h1>
34{{else}}
35<h1>Create: {{page.title}}</h1>
36{{endif}}
37
38<form action="{{req.path_url}}" method="POST">
39 <input type="hidden" name="mtime" value="{{page.mtime}}">
40 Title: <input type="text" name="title" style="width: 70%" value="{{page.title}}"><br>
41 Content: <input type="submit" value="Save">
42 <a href="{{req.path_url}}">Cancel</a>
43   <br>
44 <textarea name="content" style="width: 100%; height: 75%" rows="40">{{page.content}}</textarea>
45   <br>
46 <input type="submit" value="Save">
47 <a href="{{req.path_url}}">Cancel</a>
48</form>
49</body></html>
50""")
51
52class WikiApp(object):
53
54    view_template = VIEW_TEMPLATE
55    edit_template = EDIT_TEMPLATE
56
57    def __init__(self, storage_dir):
58        self.storage_dir = os.path.abspath(os.path.normpath(storage_dir))
59
60    def __call__(self, environ, start_response):
61        req = Request(environ)
62        action = req.params.get('action', 'view')
63        page = self.get_page(req.path_info)
64        try:
65            try:
66                meth = getattr(self, 'action_%s_%s' % (action, req.method))
67            except AttributeError:
68                raise exc.HTTPBadRequest('No such action %r' % action)
69            resp = meth(req, page)
70        except exc.HTTPException, e:
71            resp = e
72        return resp(environ, start_response)
73
74    def get_page(self, path):
75        path = path.lstrip('/')
76        if not path:
77            path = 'index'
78        path = os.path.join(self.storage_dir, path)
79        path = os.path.normpath(path)
80        if path.endswith('/'):
81            path += 'index'
82        if not path.startswith(self.storage_dir):
83            raise exc.HTTPBadRequest("Bad path")
84        path += '.html'
85        return Page(path)
86
87    def action_view_GET(self, req, page):
88        if not page.exists:
89            return exc.HTTPTemporaryRedirect(
90                location=req.url + '?action=edit')
91        if req.cookies.get('message'):
92            message = req.cookies['message']
93        else:
94            message = None
95        text = self.view_template.substitute(
96            page=page, req=req, message=message)
97        resp = Response(text)
98        if message:
99            resp.delete_cookie('message')
100        else:
101            resp.last_modified = page.mtime
102            resp.conditional_response = True
103        return resp
104
105    def action_view_POST(self, req, page):
106        submit_mtime = int(req.params.get('mtime') or '0') or None
107        if page.mtime != submit_mtime:
108            return exc.HTTPPreconditionFailed(
109                "The page has been updated since you started editing it")
110        page.set(
111            title=req.params['title'],
112            content=req.params['content'])
113        resp = exc.HTTPSeeOther(
114            location=req.path_url)
115        resp.set_cookie('message', 'Page updated')
116        return resp
117
118    def action_edit_GET(self, req, page):
119        text = self.edit_template.substitute(
120            page=page, req=req)
121        return Response(text)
122
123class Page(object):
124    def __init__(self, filename):
125        self.filename = filename
126
127    @property
128    def exists(self):
129        return os.path.exists(self.filename)
130
131    @property
132    def title(self):
133        if not self.exists:
134            # we need to guess the title
135            basename = os.path.splitext(os.path.basename(self.filename))[0]
136            basename = re.sub(r'[_-]', ' ', basename)
137            return basename.capitalize()
138        content = self.full_content
139        match = re.search(r'<title>(.*?)</title>', content, re.I|re.S)
140        return match.group(1)
141
142    @property
143    def full_content(self):
144        f = open(self.filename, 'rb')
145        try:
146            return f.read()
147        finally:
148            f.close()
149
150    @property
151    def content(self):
152        if not self.exists:
153            return ''
154        content = self.full_content
155        match = re.search(r'<body[^>]*>(.*?)</body>', content, re.I|re.S)
156        return match.group(1)
157
158    @property
159    def mtime(self):
160        if not self.exists:
161            return None
162        else:
163            return int(os.stat(self.filename).st_mtime)
164
165    def set(self, title, content):
166        dir = os.path.dirname(self.filename)
167        if not os.path.exists(dir):
168            os.makedirs(dir)
169        new_content = """<html><head><title>%s</title></head><body>%s</body></html>""" % (
170            title, content)
171        f = open(self.filename, 'wb')
172        f.write(new_content)
173        f.close()
174
175if __name__ == '__main__':
176    import optparse
177    parser = optparse.OptionParser(
178        usage='%prog --port=PORT'
179        )
180    parser.add_option(
181        '-p', '--port',
182        default='8080',
183        dest='port',
184        type='int',
185        help='Port to serve on (default 8080)')
186    parser.add_option(
187        '--wiki-data',
188        default='./wiki',
189        dest='wiki_data',
190        help='Place to put wiki data into (default ./wiki/)')
191    options, args = parser.parse_args()
192    print 'Writing wiki pages to %s' % options.wiki_data
193    app = WikiApp(options.wiki_data)
194    from wsgiref.simple_server import make_server
195    httpd = make_server('localhost', options.port, app)
196    print 'Serving on http://localhost:%s' % options.port
197    try:
198        httpd.serve_forever()
199    except KeyboardInterrupt:
200        print '^C'
201