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