]> git.ipfire.org Git - ipfire.org.git/blob - src/web/wiki.py
wiki: Add preview for editing pages
[ipfire.org.git] / src / web / wiki.py
1 #!/usr/bin/python3
2
3 import difflib
4 import tornado.web
5
6 from . import auth
7 from . import base
8 from . import ui_modules
9
10 class ActionEditHandler(auth.CacheMixin, base.BaseHandler):
11 @tornado.web.authenticated
12 def get(self, path):
13 # Check permissions
14 if not self.backend.wiki.check_acl(path, self.current_user):
15 raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
16
17 # Fetch the wiki page
18 page = self.backend.wiki.get_page(path)
19
20 # Empty page if it was deleted
21 if page and page.was_deleted():
22 page = None
23
24 # Render page
25 self.render("wiki/edit.html", page=page)
26
27 @tornado.web.authenticated
28 def post(self, path):
29 # Check permissions
30 if not self.backend.wiki.check_acl(path, self.current_user):
31 raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
32
33 content = self.get_argument("content", None)
34 changes = self.get_argument("changes")
35
36 # Create a new page in the database
37 with self.db.transaction():
38 page = self.backend.wiki.create_page(path,
39 self.current_user, content, changes=changes, address=self.get_remote_ip())
40
41 # Add user as a watcher if wanted
42 watch = self.get_argument("watch", False)
43 if watch:
44 page.add_watcher(self.current_user)
45
46 # Redirect back
47 if page.was_deleted():
48 self.redirect("/")
49 else:
50 self.redirect(page.url)
51
52 def on_finish(self):
53 """
54 Updates the search index after the page has been edited
55 """
56 # This is being executed in the background and after
57 # the response has been set to the client
58 with self.db.transaction():
59 self.backend.wiki.refresh()
60
61
62 class ActionUploadHandler(auth.CacheMixin, base.BaseHandler):
63 @tornado.web.authenticated
64 def post(self):
65 path = self.get_argument("path")
66
67 # Check permissions
68 if not self.backend.wiki.check_acl(path, self.current_user):
69 raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
70
71 try:
72 filename, data, mimetype = self.get_file("file")
73
74 # XXX check valid mimetypes
75
76 with self.db.transaction():
77 file = self.backend.wiki.upload(path, filename, data,
78 mimetype=mimetype, author=self.current_user,
79 address=self.get_remote_ip())
80
81 except TypeError as e:
82 raise e
83
84 self.redirect("%s/_files" % path)
85
86
87 class ActionWatchHandler(auth.CacheMixin, base.BaseHandler):
88 @tornado.web.authenticated
89 def get(self, path, action):
90 page = self.backend.wiki.get_page(path)
91 if not page:
92 raise tornado.web.HTTPError(404, "Page does not exist: %s" % path)
93
94 # Check permissions
95 if not self.backend.wiki.check_acl(path, self.current_user):
96 raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
97
98 with self.db.transaction():
99 if action == "watch":
100 page.add_watcher(self.current_user)
101 elif action == "unwatch":
102 page.remove_watcher(self.current_user)
103
104 # Redirect back to page
105 self.redirect(page.url)
106
107
108 class ActionRenderHandler(auth.CacheMixin, base.BaseHandler):
109 def check_xsrf_cookie(self):
110 pass # disabled
111
112 @tornado.web.authenticated
113 def post(self, path):
114 content = self.get_argument("content")
115
116 # Render the content
117 html = self.backend.wiki.render(path, content)
118
119 self.finish(html)
120
121
122 class FilesHandler(auth.CacheMixin, base.BaseHandler):
123 @tornado.web.authenticated
124 def get(self, path):
125 # Check permissions
126 if not self.backend.wiki.check_acl(path, self.current_user):
127 raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
128
129 files = self.backend.wiki.get_files(path)
130
131 self.render("wiki/files/index.html", path=path, files=files)
132
133
134 class FileHandler(base.BaseHandler):
135 @property
136 def action(self):
137 return self.get_argument("action", None)
138
139 def get(self, path):
140 # Check permissions
141 if not self.backend.wiki.check_acl(path, self.current_user):
142 raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
143
144 # Fetch the file
145 file = self.backend.wiki.get_file_by_path(path)
146 if not file:
147 raise tornado.web.HTTPError(404, "Could not find %s" % path)
148
149 # Render detail page
150 if self.action == "detail":
151 page = None
152
153 for breadcrumb, title in self.backend.wiki.make_breadcrumbs(path):
154 page = self.backend.wiki.get_page(breadcrumb)
155 if page:
156 break
157
158 self.render("wiki/files/detail.html", page=page, file=file)
159 return
160
161 size = self.get_argument_int("s", None)
162
163 # Check if image should be resized
164 if file.is_image() and size:
165 blob = file.get_thumbnail(size)
166 else:
167 blob = file.blob
168
169 # Set headers
170 self.set_header("Content-Type", file.mimetype or "application/octet-stream")
171 self.set_header("Content-Length", len(blob))
172
173 # Set expires
174 self.set_expires(3600)
175
176 # Deliver content
177 self.finish(blob)
178
179
180 class PageHandler(auth.CacheMixin, base.BaseHandler):
181 @property
182 def action(self):
183 return self.get_argument("action", None)
184
185 def write_error(self, status_code, **kwargs):
186 # Render a custom page for 404
187 if status_code == 404:
188 self.render("wiki/404.html", **kwargs)
189 return
190
191 # Otherwise raise this to one layer above
192 super().write_error(status_code, **kwargs)
193
194 @tornado.web.removeslash
195 def get(self, path):
196 # Check permissions
197 if not self.backend.wiki.check_acl(path, self.current_user):
198 raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
199
200 # Check if we are asked to render a certain revision
201 revision = self.get_argument("revision", None)
202
203 # Fetch the wiki page
204 page = self.backend.wiki.get_page(path, revision=revision)
205
206 # Diff
207 if self.action == "diff":
208 # Get both revisions
209 a = self.get_argument("a")
210 b = self.get_argument("b")
211
212 # Fetch both versions of the page
213 a = self.backend.wiki.get_page(path, revision=a)
214 b = self.backend.wiki.get_page(path, revision=b)
215 if not a or not b:
216 raise tornado.web.HTTPError(404)
217
218 # Cannot render a diff for the identical page
219 if a == b:
220 raise tornado.web.HTTPError(400)
221
222 # Make sure that b is newer than a
223 if a > b:
224 a, b = b, a
225
226 self.render("wiki/diff.html", page=page, a=a, b=b)
227 return
228
229 # Revisions
230 elif self.action == "revisions":
231 self.render("wiki/revisions.html", page=page)
232 return
233
234 # If the page does not exist, we send 404
235 if not page or page.was_deleted():
236 raise tornado.web.HTTPError(404)
237
238 # Fetch the latest revision
239 latest_revision = page.get_latest_revision()
240
241 # Render page
242 self.render("wiki/page.html", page=page, latest_revision=latest_revision)
243
244
245 class SearchHandler(auth.CacheMixin, base.BaseHandler):
246 @base.blacklisted
247 def get(self):
248 q = self.get_argument("q")
249
250 pages = self.backend.wiki.search(q, account=self.current_user, limit=50)
251
252 self.render("wiki/search-results.html", q=q, pages=pages)
253
254
255 class RecentChangesHandler(auth.CacheMixin, base.BaseHandler):
256 def get(self):
257 recent_changes = self.backend.wiki.get_recent_changes(self.current_user, limit=50)
258
259 self.render("wiki/recent-changes.html", recent_changes=recent_changes)
260
261
262 class WatchlistHandler(auth.CacheMixin, base.BaseHandler):
263 @tornado.web.authenticated
264 def get(self):
265 pages = self.backend.wiki.get_watchlist(self.current_user)
266
267 self.render("wiki/watchlist.html", pages=pages)
268
269
270 class WikiDiffModule(ui_modules.UIModule):
271 differ = difflib.Differ()
272
273 def render(self, a, b):
274 diff = self.differ.compare(
275 a.markdown.splitlines(),
276 b.markdown.splitlines(),
277 )
278
279 return self.render_string("wiki/modules/diff.html", diff=diff)
280
281
282 class WikiListModule(ui_modules.UIModule):
283 def render(self, pages, link_revision=False, show_breadcrumbs=True,
284 show_author=True, show_changes=False):
285 return self.render_string("wiki/modules/list.html", link_revision=link_revision,
286 pages=pages, show_breadcrumbs=show_breadcrumbs,
287 show_author=show_author, show_changes=show_changes)
288
289
290 class WikiNavbarModule(ui_modules.UIModule):
291 @property
292 def path(self):
293 """
294 Returns the path of the page (without any actions)
295 """
296 path = self.request.path.split("/")
297
298 if path and path[-1].startswith("_"):
299 path.pop()
300
301 return "/".join(path)
302
303 def render(self, suffix=None):
304 _ = self.locale.translate
305
306 # Make the path
307 page = self.request.path.split("/")
308
309 # Drop the action bit
310 if page and page[-1].startswith("_"):
311 page.pop()
312
313 page = "/".join(page)
314
315 breadcrumbs = self.backend.wiki.make_breadcrumbs(page)
316 title = self.backend.wiki.get_page_title(page)
317
318 if self.request.path.endswith("/_edit"):
319 suffix = _("Edit")
320 elif self.request.path.endswith("/_files"):
321 suffix = _("Files")
322
323 return self.render_string("wiki/modules/navbar.html",
324 breadcrumbs=breadcrumbs, page=page, page_title=title, suffix=suffix)