]> git.ipfire.org Git - ipfire.org.git/blame - src/web/docs.py
wiki: Only match usernames when a word starts with @
[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 8
28e09035 9class PageHandler(base.AnalyticsMixin, base.BaseHandler):
1958a22b
MT
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
19986246
MT
79 page = self.backend.wiki.get_page(path)
80 if page and not page.was_deleted():
81 self.redirect(page.url)
1958a22b
MT
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
28e09035 107class FileHandler(base.AnalyticsMixin, base.BaseHandler):
4a1bfdd5
MT
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
ccfb1584 137 # Get image size
4a1bfdd5
MT
138 size = self.get_argument_int("s", None)
139
140 # Check if image should be resized
141 if size and file.is_bitmap_image():
ccfb1584
MT
142 # Send WEBP if the browser supports
143 if self.browser_accepts("image/webp"):
144 format = "WEBP"
145
146 # Fall back to the native format
147 else:
148 format = None
149
150 blob = await file.get_thumbnail(size, format=format)
4a1bfdd5
MT
151 else:
152 blob = file.blob
153
92016cb9
MT
154 # Allow downstream to cache this for a year
155 if revision:
156 self.set_expires(31536000)
4a1bfdd5 157
c6653ffc
MT
158 # Send the payload
159 self._deliver_file(blob, filename=file.filename)
4a1bfdd5
MT
160
161
ec17fa2b 162class EditHandler(base.BaseHandler):
d7580469
MT
163 @tornado.web.authenticated
164 def get(self, path):
165 if path is None:
166 path = "/"
167
168 # Check permissions
169 if not self.backend.wiki.check_acl(path, self.current_user):
170 raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
171
172 # Fetch the wiki page
173 page = self.backend.wiki.get_page(path)
174
175 # Empty page if it was deleted
176 if page and page.was_deleted():
177 page = None
178
179 # Render page
180 self.render("docs/edit.html", page=page, path=path)
181
182 @tornado.web.authenticated
183 def post(self, path):
184 if path is None:
185 path = "/"
186
187 # Check permissions
188 if not self.backend.wiki.check_acl(path, self.current_user):
189 raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
190
191 content = self.get_argument("content", None)
192 changes = self.get_argument("changes")
193
194 # Create a new page in the database
195 with self.db.transaction():
196 page = self.backend.wiki.create_page(path,
197 self.current_user, content, changes=changes, address=self.get_remote_ip())
198
199 # Add user as a watcher if wanted
200 watch = self.get_argument("watch", False)
201 if watch:
202 page.add_watcher(self.current_user)
203
204 # Redirect back
205 if page.was_deleted():
206 self.redirect("/")
207 else:
208 self.redirect(page.url)
209
210 def on_finish(self):
211 """
212 Updates the search index after the page has been edited
213 """
214 # This is being executed in the background and after
215 # the response has been set to the client
216 with self.db.transaction():
217 self.backend.wiki.refresh()
218
219
ec17fa2b 220class RenderHandler(base.BaseHandler):
d7580469
MT
221 def check_xsrf_cookie(self):
222 pass # disabled
223
224 @tornado.web.authenticated
225 @base.ratelimit(minutes=5, requests=180)
226 def post(self, path):
227 if path is None:
228 path = "/"
229
230 content = self.get_argument("content")
231
232 # Render the content
5ab70651 233 renderer = self.backend.wiki.render(path, content)
d7580469 234
5ab70651 235 self.finish(renderer.html)
d7580469
MT
236
237
bb998aca
MT
238class RestoreHandler(base.BaseHandler):
239 @tornado.web.authenticated
240 @base.ratelimit(minutes=60, requests=24)
241 def post(self):
242 path = self.get_argument("path")
243
244 # Check permissions
245 if not self.backend.wiki.check_acl(path, self.current_user):
246 raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
247
248 # Check if we are asked to render a certain revision
249 revision = self.get_argument("revision", None)
250 comment = self.get_argument("comment", None)
251
252 # Fetch the wiki page
253 page = self.backend.wiki.get_page(path, revision=revision)
254
255 with self.db.transaction():
256 page = page.restore(
257 author=self.current_user,
258 address=self.get_remote_ip(),
259 comment=comment,
260 )
261
262 # Redirect back to page
263 self.redirect(page.page)
264
265
26ade731
MT
266class UploadHandler(base.BaseHandler):
267 @tornado.web.authenticated
268 @base.ratelimit(minutes=60, requests=24)
269 def post(self):
270 path = self.get_argument("path")
271
272 # Check permissions
273 if not self.backend.wiki.check_acl(path, self.current_user):
274 raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
275
276 try:
277 filename, data, mimetype = self.get_file("file")
278
279 # Use filename from request if any
280 filename = self.get_argument("filename", filename)
281
282 # XXX check valid mimetypes
283
284 with self.db.transaction():
285 file = self.backend.wiki.upload(path, filename, data,
286 mimetype=mimetype, author=self.current_user,
287 address=self.get_remote_ip())
288
289 except TypeError as e:
290 raise e
291
99d40bec 292 self.redirect("/docs%s/_files" % path)
26ade731
MT
293
294
ec17fa2b 295class WatchHandler(base.BaseHandler):
01ddad9b
MT
296 @tornado.web.authenticated
297 @base.ratelimit(minutes=60, requests=180)
298 def get(self, path, action):
299 if path is None:
300 path = "/"
301
302 page = self.backend.wiki.get_page(path)
303 if not page:
304 raise tornado.web.HTTPError(404, "Page does not exist: %s" % path)
305
306 # Check permissions
307 if not self.backend.wiki.check_acl(path, self.current_user):
308 raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
309
310 with self.db.transaction():
311 if action == "watch":
312 page.add_watcher(self.current_user)
313 elif action == "unwatch":
314 page.remove_watcher(self.current_user)
315
316 # Redirect back to page
317 self.redirect(page.url)
318
319
16510618
MT
320class DeleteFileHandler(base.BaseHandler):
321 @tornado.web.authenticated
322 def get(self, path):
323 # Check permissions
324 if not self.backend.wiki.check_acl(path, self.current_user):
325 raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
326
327 # Fetch the file
328 file = self.backend.wiki.get_file_by_path(path)
329 if not file:
330 raise tornado.web.HTTPError(404, "Could not find %s" % path)
331
09325b8c
MT
332 # Check if this can be deleted
333 if not file.can_be_deleted():
334 raise tornado.web.HTTPError(400, "%s cannot be deleted" % file)
335
16510618
MT
336 self.render("docs/confirm-delete.html", file=file)
337
338 @tornado.web.authenticated
339 @base.ratelimit(minutes=60, requests=24)
340 def post(self, path):
341 # Check permissions
342 if not self.backend.wiki.check_acl(path, self.current_user):
343 raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
344
345 # Fetch the file
346 file = self.backend.wiki.get_file_by_path(path)
347 if not file:
348 raise tornado.web.HTTPError(404, "Could not find %s" % path)
349
09325b8c
MT
350 # Check if this can be deleted
351 if not file.can_be_deleted():
352 raise tornado.web.HTTPError(400, "%s cannot be deleted" % file)
353
16510618
MT
354 with self.db.transaction():
355 file.delete(self.current_user)
356
08e086b6 357 self.redirect("/docs%s/_files" % file.path)
16510618
MT
358
359
28e09035 360class SearchHandler(base.AnalyticsMixin, base.BaseHandler):
0ce8cc32
MT
361 @base.ratelimit(minutes=5, requests=25)
362 def get(self):
363 q = self.get_argument("q")
364
365 # Perform search
366 with self.db.transaction():
367 pages = self.backend.wiki.search(q, account=self.current_user, limit=50)
368
369 self.render("docs/search-results.html", q=q, pages=pages)
370
371
28e09035 372class RecentChangesHandler(base.AnalyticsMixin, base.BaseHandler):
5f0d294e
MT
373 def get(self):
374 recent_changes = self.backend.wiki.get_recent_changes(self.current_user, limit=50)
375
376 self.render("docs/recent-changes.html", recent_changes=recent_changes)
377
378
28e09035 379class TreeHandler(base.AnalyticsMixin, base.BaseHandler):
350f391e
MT
380 def get(self):
381 self.render("docs/tree.html", pages=self.backend.wiki)
382
383
28e09035 384class WatchlistHandler(base.AnalyticsMixin, base.BaseHandler):
16619e57
MT
385 @tornado.web.authenticated
386 def get(self):
387 pages = self.backend.wiki.get_watchlist(self.current_user)
388
389 self.render("docs/watchlist.html", pages=pages)
390
391
d25f886f
MT
392class ListModule(ui_modules.UIModule):
393 def render(self, pages, link_revision=False, show_breadcrumbs=True,
394 show_author=True, show_changes=False):
395 return self.render_string("docs/modules/list.html", link_revision=link_revision,
396 pages=pages, show_breadcrumbs=show_breadcrumbs,
397 show_author=show_author, show_changes=show_changes)
398
399
cf59466c
MT
400class HeaderModule(ui_modules.UIModule):
401 @property
402 def page(self):
403 """
404 Returns the path of the page (without any actions)
405 """
406 path = self.request.path.removeprefix("/docs")
407
408 return "/".join((p for p in path.split("/") if not p.startswith("_")))
409
410 def render(self, suffix=None):
411 _ = self.locale.translate
412
413 breadcrumbs = self.backend.wiki.make_breadcrumbs(self.page)
414 title = self.backend.wiki.get_page_title(self.page)
415
416 if self.request.path.endswith("/_edit"):
417 suffix = _("Edit")
418 elif self.request.path.endswith("/_files"):
419 suffix = _("Files")
420
421 return self.render_string("docs/modules/header.html",
422 breadcrumbs=breadcrumbs, page=self.page, page_title=title, suffix=suffix)
739fff76
MT
423
424
425class DiffModule(ui_modules.UIModule):
426 differ = difflib.Differ()
427
428 def render(self, a, b):
429 diff = self.differ.compare(
430 a.markdown.splitlines(),
431 b.markdown.splitlines(),
432 )
433
434 return self.render_string("docs/modules/diff.html", diff=diff)