From 181d08f355f5f50b332cd9eeb50772cd857d1277 Mon Sep 17 00:00:00 2001 From: Michael Tremer Date: Mon, 12 Nov 2018 15:07:34 +0000 Subject: [PATCH] Implement a basic wiki Signed-off-by: Michael Tremer --- Makefile.am | 10 ++- src/backend/base.py | 2 + src/backend/wiki.py | 159 +++++++++++++++++++++++++++++++++++ src/templates/base.html | 18 ++++ src/templates/wiki/base.html | 13 +++ src/templates/wiki/page.html | 11 +++ src/web/__init__.py | 12 +++ src/web/wiki.py | 50 +++++++++++ 8 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 src/backend/wiki.py create mode 100644 src/templates/wiki/base.html create mode 100644 src/templates/wiki/page.html create mode 100644 src/web/wiki.py diff --git a/Makefile.am b/Makefile.am index 6867394d..621c610d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -66,6 +66,7 @@ backend_PYTHON = \ src/backend/talk.py \ src/backend/tracker.py \ src/backend/util.py \ + src/backend/wiki.py \ src/backend/zeiterfassung.py backenddir = $(pythondir)/ipfire @@ -88,7 +89,8 @@ web_PYTHON = \ src/web/newsletter.py \ src/web/nopaste.py \ src/web/people.py \ - src/web/ui_modules.py + src/web/ui_modules.py \ + src/web/wiki.py webdir = $(backenddir)/web @@ -251,6 +253,12 @@ templates_static_DATA = \ templates_staticdir = $(templatesdir)/static +templates_wiki_DATA = \ + src/templates/wiki/base.html \ + src/templates/wiki/page.html + +templates_wikidir = $(templatesdir)/wiki + # ------------------------------------------------------------------------------ SCSS_FILES = \ diff --git a/src/backend/base.py b/src/backend/base.py index 0507d5cd..9e4ae21b 100644 --- a/src/backend/base.py +++ b/src/backend/base.py @@ -17,6 +17,7 @@ from . import settings from . import talk from . import blog +from . import wiki from . import zeiterfassung DEFAULT_CONFIG = io.StringIO(""" @@ -54,6 +55,7 @@ class Backend(object): self.talk = talk.Talk(self) self.blog = blog.Blog(self) + self.wiki = wiki.Wiki(self) self.zeiterfassung = zeiterfassung.ZeiterfassungClient(self) def read_config(self, configfile): diff --git a/src/backend/wiki.py b/src/backend/wiki.py new file mode 100644 index 00000000..a5635beb --- /dev/null +++ b/src/backend/wiki.py @@ -0,0 +1,159 @@ +#!/usr/bin/python3 + +import logging +import markdown2 +import re + +from . import misc +from .decorators import * + +# Used to automatically link some things +link_patterns = ( + # Find bug reports + (re.compile(r"(?:#(\d+))", re.I), r"https://bugzilla.ipfire.org/show_bug.cgi?id=\1"), + + # Email Addresses + (re.compile(r"([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)"), r"mailto:\1"), + + # CVE Numbers + (re.compile(r"(?:CVE)[\s\-](\d{4}\-\d+)"), r"https://cve.mitre.org/cgi-bin/cvename.cgi?name=\1"), +) + +class Wiki(misc.Object): + def _get_pages(self, query, *args): + res = self.db.query(query, *args) + + for row in res: + yield Page(self.backend, row.id, data=row) + + def get_page(self, page, revision=None): + page = Page.sanitise_page_name(page) + assert page + + if revision: + res = self.db.get("SELECT * FROM wiki WHERE page = %s \ + AND timestamp = %s", page, revision) + else: + res = self.db.get("SELECT * FROM wiki WHERE page = %s \ + ORDER BY timestamp DESC LIMIT 1", page) + + if res: + return Page(self.backend, res.id, data=res) + + def get_recent_changes(self): + return self._get_pages("SELECT * FROM wiki \ + WHERE timestamp >= NOW() - INTERVAL '4 weeks' ORDER BY timestamp DESC") + + def create_page(self, page, author, markdown): + page = Page.sanitise_page_name(page) + + res = self.db.get("INSERT INTO wiki(page, author_id, markdown) \ + VALUES(%s, %s, %s) RETURNING id", page, author.id, markdown) + + if res: + return self.get_page_by_id(res.id) + + def delete_page(self, page, author): + # Do nothing if the page does not exist + if not self.get_page(page): + return + + # Just creates a blank last version of the page + self.create_page(page, author, None) + + @staticmethod + def _split_url(url): + parts = list(e for e in url.split("/") if e) + + num_parts = len(parts) + for i in range(num_parts): + yield "/".join(parts[:i]) + + def make_breadcrumbs(self, url): + for part in self._split_url(url): + title = self.get_page_title(part, os.path.basename(part)) + + yield ("/%s" % part, title) + + +class Page(misc.Object): + def init(self, id, data=None): + self.id = id + self.data = data + + def __lt__(self, other): + if isinstance(other, self.__class__): + if self.page == other.page: + return self.timestamp < other.timestamp + + return self.page < other.page + + @staticmethod + def sanitise_page_name(page): + if not page: + return "/" + + # Make sure that the page name does NOT end with a / + if page.endswith("/"): + page = page[:-1] + + # Make sure the page name starts with a / + if not page.startswith("/"): + page = "/%s" % page + + # Remove any double slashes + page = page.replace("//", "/") + + return page + + @property + def url(self): + return "/%s" % self.page + + @property + def page(self): + return self.data.page + + @property + def title(self): + return self._title or self.page[1:] + + @property + def _title(self): + if not self.markdown: + return + + # Find first H1 headline in markdown + markdown = self.markdown.splitlines() + + m = re.match(r"^# (.*)( #)?$", markdown[0]) + if m: + return m.group(1) + + def _render(self, text): + logging.debug("Rendering %s" % self) + + return markdown2.markdown(text, link_patterns=link_patterns, + extras=["footnotes", "link-patterns", "wiki-tables"]) + + @property + def markdown(self): + return self.data.markdown + + @property + def html(self): + return self.data.html or self._render(self.markdown) + + @property + def timestamp(self): + return self.data.timestamp + + def was_deleted(self): + return self.markdown is None + + @lazy_property + def breadcrumbs(self): + return self.backend.wiki.make_breadcrumbs(self.page) + + def get_latest_revision(self): + return self.backend.wiki.get_page(self.page) diff --git a/src/templates/base.html b/src/templates/base.html index ffe6fb67..806b98e7 100644 --- a/src/templates/base.html +++ b/src/templates/base.html @@ -29,6 +29,8 @@ {{ _("Mirrors") }} {% elif hostname == "people.ipfire.org" %} {{ _("People") }} + {% elif hostname == "wiki.ipfire.org" %} + {{ _("Wiki") }} {% end %} @@ -188,6 +190,22 @@ {% end %} + {% elif hostname == "wiki.ipfire.org" %} + + + {% end %} {% end block %} diff --git a/src/templates/wiki/base.html b/src/templates/wiki/base.html new file mode 100644 index 00000000..dd35246f --- /dev/null +++ b/src/templates/wiki/base.html @@ -0,0 +1,13 @@ +{% extends "../base.html" %} + +{% block content %} +
+
+ {% block sidebar %}{% end block %} +
+ +
+ {% block main %}{% end block %} +
+
+{% end block %} diff --git a/src/templates/wiki/page.html b/src/templates/wiki/page.html new file mode 100644 index 00000000..575863ec --- /dev/null +++ b/src/templates/wiki/page.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} + +{% block title %}{{ page.title }}{% end block %} + +{% block main %} +
+
+ {% raw page.html %} +
+
+{% end block %} diff --git a/src/web/__init__.py b/src/web/__init__.py index d7c2407b..1c054b8f 100644 --- a/src/web/__init__.py +++ b/src/web/__init__.py @@ -27,6 +27,7 @@ from . import newsletter from . import nopaste from . import people from . import ui_modules +from . import wiki class Application(tornado.web.Application): def __init__(self, config, **kwargs): @@ -275,6 +276,17 @@ class Application(tornado.web.Application): (r"/users/(\w+)/sip", people.SIPHandler), ] + authentication_handlers) + # wiki.ipfire.org + self.add_handlers(r"wiki(\.dev)?\.ipfire\.org", + authentication_handlers + [ + + # Deliver static files (CSS, etc.) + #(r"/(static/.*)", tornado.web.StaticFileHandler), + + (r"/search", wiki.SearchHandler), + (r"([A-Za-z0-9\-_\/]+)?", wiki.PageHandler), + ]) + # ipfire.org self.add_handlers(r"ipfire\.org", [ (r".*", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org" }) diff --git a/src/web/wiki.py b/src/web/wiki.py new file mode 100644 index 00000000..e8201f1d --- /dev/null +++ b/src/web/wiki.py @@ -0,0 +1,50 @@ +#!/usr/bin/python3 + +import tornado.web + +from . import auth +from . import base + +class PageHandler(auth.CacheMixin, base.BaseHandler): + @tornado.web.removeslash + def get(self, page): + page = self.backend.wiki.get_page(page) + + # If the page does not exist, we send 404 + if not page or page.was_deleted(): + raise tornado.web.HTTPError(404) + + # Fetch the latest revision + latest_revision = page.get_latest_revision() + + # Render page + self.render("wiki/page.html", page=page, latest_revision=latest_revision) + + @tornado.web.authenticated + def post(self, page): + content = self.get_argument("content", None) + + # Delete the page if content is empty + if not content: + with self.db.transaction(): + self.backend.wiki.delete_page(page, self.current_user) + + return self.redirect("/") + + # Create a new page in the database + page = self.backend.wiki.create_page(page, self.current_user, content) + + # Redirect + self.redirect(page.url) + + +class SearchHandler(auth.CacheMixin, base.BaseHandler): + @base.blacklisted + def get(self): + q = self.get_argument("q") + + pages = self.backend.wiki.search(q, limit=50) + if not pages: + raise tornado.web.HTTPError(404, "Nothing found") + + self.render("wiki/search-results.html", q=q, pages=pages) -- 2.39.2