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