]> git.ipfire.org Git - ipfire.org.git/blob - src/web/docs.py
docs: Add a way to extract linked images
[ipfire.org.git] / src / web / docs.py
1 #!/usr/bin/python3
2
3 import difflib
4 import tornado.web
5
6 from . import base
7 from . import ui_modules
8
9 class 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:
17 self.render("docs/404.html", **kwargs)
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
58 self.render("docs/diff.html", page=page, a=a, b=b)
59 return
60
61 # Restore
62 elif self.action == "restore":
63 self.render("docs/confirm-restore.html", page=page)
64 return
65
66 # Revisions
67 elif self.action == "revisions":
68 self.render("docs/revisions.html", page=page)
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
89 self.render("docs/page.html", page=page, latest_revision=latest_revision)
90
91
92 class 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
107 class FileHandler(base.BaseHandler):
108 @property
109 def action(self):
110 return self.get_argument("action", None)
111
112 async def get(self, path):
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
134 self.render("docs/files/detail.html", page=page, file=file)
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():
141 blob = await file.get_thumbnail(size)
142 else:
143 blob = file.blob
144
145 # Set headers
146 self.set_header("Content-Type", file.mimetype or "application/octet-stream")
147 self.set_header("Content-Length", len(blob))
148
149 # Set expires
150 self.set_expires(3600)
151
152 # Deliver content
153 self.finish(blob)
154
155
156 class EditHandler(base.BaseHandler):
157 @tornado.web.authenticated
158 def get(self, path):
159 if path is None:
160 path = "/"
161
162 # Check permissions
163 if not self.backend.wiki.check_acl(path, self.current_user):
164 raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
165
166 # Fetch the wiki page
167 page = self.backend.wiki.get_page(path)
168
169 # Empty page if it was deleted
170 if page and page.was_deleted():
171 page = None
172
173 # Render page
174 self.render("docs/edit.html", page=page, path=path)
175
176 @tornado.web.authenticated
177 def post(self, path):
178 if path is None:
179 path = "/"
180
181 # Check permissions
182 if not self.backend.wiki.check_acl(path, self.current_user):
183 raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
184
185 content = self.get_argument("content", None)
186 changes = self.get_argument("changes")
187
188 # Create a new page in the database
189 with self.db.transaction():
190 page = self.backend.wiki.create_page(path,
191 self.current_user, content, changes=changes, address=self.get_remote_ip())
192
193 # Add user as a watcher if wanted
194 watch = self.get_argument("watch", False)
195 if watch:
196 page.add_watcher(self.current_user)
197
198 # Redirect back
199 if page.was_deleted():
200 self.redirect("/")
201 else:
202 self.redirect(page.url)
203
204 def on_finish(self):
205 """
206 Updates the search index after the page has been edited
207 """
208 # This is being executed in the background and after
209 # the response has been set to the client
210 with self.db.transaction():
211 self.backend.wiki.refresh()
212
213
214 class RenderHandler(base.BaseHandler):
215 def check_xsrf_cookie(self):
216 pass # disabled
217
218 @tornado.web.authenticated
219 @base.ratelimit(minutes=5, requests=180)
220 def post(self, path):
221 if path is None:
222 path = "/"
223
224 content = self.get_argument("content")
225
226 # Render the content
227 renderer = self.backend.wiki.render(path, content)
228
229 self.finish(renderer.html)
230
231
232 class RestoreHandler(base.BaseHandler):
233 @tornado.web.authenticated
234 @base.ratelimit(minutes=60, requests=24)
235 def post(self):
236 path = self.get_argument("path")
237
238 # Check permissions
239 if not self.backend.wiki.check_acl(path, self.current_user):
240 raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
241
242 # Check if we are asked to render a certain revision
243 revision = self.get_argument("revision", None)
244 comment = self.get_argument("comment", None)
245
246 # Fetch the wiki page
247 page = self.backend.wiki.get_page(path, revision=revision)
248
249 with self.db.transaction():
250 page = page.restore(
251 author=self.current_user,
252 address=self.get_remote_ip(),
253 comment=comment,
254 )
255
256 # Redirect back to page
257 self.redirect(page.page)
258
259
260 class UploadHandler(base.BaseHandler):
261 @tornado.web.authenticated
262 @base.ratelimit(minutes=60, requests=24)
263 def post(self):
264 path = self.get_argument("path")
265
266 # Check permissions
267 if not self.backend.wiki.check_acl(path, self.current_user):
268 raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
269
270 try:
271 filename, data, mimetype = self.get_file("file")
272
273 # Use filename from request if any
274 filename = self.get_argument("filename", filename)
275
276 # XXX check valid mimetypes
277
278 with self.db.transaction():
279 file = self.backend.wiki.upload(path, filename, data,
280 mimetype=mimetype, author=self.current_user,
281 address=self.get_remote_ip())
282
283 except TypeError as e:
284 raise e
285
286 self.redirect("/docs%s/_files" % path)
287
288
289 class WatchHandler(base.BaseHandler):
290 @tornado.web.authenticated
291 @base.ratelimit(minutes=60, requests=180)
292 def get(self, path, action):
293 if path is None:
294 path = "/"
295
296 page = self.backend.wiki.get_page(path)
297 if not page:
298 raise tornado.web.HTTPError(404, "Page does not exist: %s" % path)
299
300 # Check permissions
301 if not self.backend.wiki.check_acl(path, self.current_user):
302 raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
303
304 with self.db.transaction():
305 if action == "watch":
306 page.add_watcher(self.current_user)
307 elif action == "unwatch":
308 page.remove_watcher(self.current_user)
309
310 # Redirect back to page
311 self.redirect(page.url)
312
313
314 class DeleteFileHandler(base.BaseHandler):
315 @tornado.web.authenticated
316 def get(self, path):
317 # Check permissions
318 if not self.backend.wiki.check_acl(path, self.current_user):
319 raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
320
321 # Fetch the file
322 file = self.backend.wiki.get_file_by_path(path)
323 if not file:
324 raise tornado.web.HTTPError(404, "Could not find %s" % path)
325
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
340 with self.db.transaction():
341 file.delete(self.current_user)
342
343 self.redirect("/docs%s/_files" % file.path)
344
345
346 class SearchHandler(base.BaseHandler):
347 @base.ratelimit(minutes=5, requests=25)
348 def get(self):
349 q = self.get_argument("q")
350
351 # Perform search
352 with self.db.transaction():
353 pages = self.backend.wiki.search(q, account=self.current_user, limit=50)
354
355 self.render("docs/search-results.html", q=q, pages=pages)
356
357
358 class RecentChangesHandler(base.BaseHandler):
359 def get(self):
360 recent_changes = self.backend.wiki.get_recent_changes(self.current_user, limit=50)
361
362 self.render("docs/recent-changes.html", recent_changes=recent_changes)
363
364
365 class TreeHandler(base.BaseHandler):
366 def get(self):
367 self.render("docs/tree.html", pages=self.backend.wiki)
368
369
370 class WatchlistHandler(base.BaseHandler):
371 @tornado.web.authenticated
372 def get(self):
373 pages = self.backend.wiki.get_watchlist(self.current_user)
374
375 self.render("docs/watchlist.html", pages=pages)
376
377
378 class ListModule(ui_modules.UIModule):
379 def render(self, pages, link_revision=False, show_breadcrumbs=True,
380 show_author=True, show_changes=False):
381 return self.render_string("docs/modules/list.html", link_revision=link_revision,
382 pages=pages, show_breadcrumbs=show_breadcrumbs,
383 show_author=show_author, show_changes=show_changes)
384
385
386 class HeaderModule(ui_modules.UIModule):
387 @property
388 def page(self):
389 """
390 Returns the path of the page (without any actions)
391 """
392 path = self.request.path.removeprefix("/docs")
393
394 return "/".join((p for p in path.split("/") if not p.startswith("_")))
395
396 def render(self, suffix=None):
397 _ = self.locale.translate
398
399 breadcrumbs = self.backend.wiki.make_breadcrumbs(self.page)
400 title = self.backend.wiki.get_page_title(self.page)
401
402 if self.request.path.endswith("/_edit"):
403 suffix = _("Edit")
404 elif self.request.path.endswith("/_files"):
405 suffix = _("Files")
406
407 return self.render_string("docs/modules/header.html",
408 breadcrumbs=breadcrumbs, page=self.page, page_title=title, suffix=suffix)
409
410
411 class DiffModule(ui_modules.UIModule):
412 differ = difflib.Differ()
413
414 def render(self, a, b):
415 diff = self.differ.compare(
416 a.markdown.splitlines(),
417 b.markdown.splitlines(),
418 )
419
420 return self.render_string("docs/modules/diff.html", diff=diff)