]> git.ipfire.org Git - ipfire.org.git/blob - src/backend/wiki.py
Implement a basic wiki
[ipfire.org.git] / src / backend / wiki.py
1 #!/usr/bin/python3
2
3 import logging
4 import markdown2
5 import re
6
7 from . import misc
8 from .decorators import *
9
10 # Used to automatically link some things
11 link_patterns = (
12 # Find bug reports
13 (re.compile(r"(?:#(\d+))", re.I), r"https://bugzilla.ipfire.org/show_bug.cgi?id=\1"),
14
15 # Email Addresses
16 (re.compile(r"([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)"), r"mailto:\1"),
17
18 # CVE Numbers
19 (re.compile(r"(?:CVE)[\s\-](\d{4}\-\d+)"), r"https://cve.mitre.org/cgi-bin/cvename.cgi?name=\1"),
20 )
21
22 class Wiki(misc.Object):
23 def _get_pages(self, query, *args):
24 res = self.db.query(query, *args)
25
26 for row in res:
27 yield Page(self.backend, row.id, data=row)
28
29 def get_page(self, page, revision=None):
30 page = Page.sanitise_page_name(page)
31 assert page
32
33 if revision:
34 res = self.db.get("SELECT * FROM wiki WHERE page = %s \
35 AND timestamp = %s", page, revision)
36 else:
37 res = self.db.get("SELECT * FROM wiki WHERE page = %s \
38 ORDER BY timestamp DESC LIMIT 1", page)
39
40 if res:
41 return Page(self.backend, res.id, data=res)
42
43 def get_recent_changes(self):
44 return self._get_pages("SELECT * FROM wiki \
45 WHERE timestamp >= NOW() - INTERVAL '4 weeks' ORDER BY timestamp DESC")
46
47 def create_page(self, page, author, markdown):
48 page = Page.sanitise_page_name(page)
49
50 res = self.db.get("INSERT INTO wiki(page, author_id, markdown) \
51 VALUES(%s, %s, %s) RETURNING id", page, author.id, markdown)
52
53 if res:
54 return self.get_page_by_id(res.id)
55
56 def delete_page(self, page, author):
57 # Do nothing if the page does not exist
58 if not self.get_page(page):
59 return
60
61 # Just creates a blank last version of the page
62 self.create_page(page, author, None)
63
64 @staticmethod
65 def _split_url(url):
66 parts = list(e for e in url.split("/") if e)
67
68 num_parts = len(parts)
69 for i in range(num_parts):
70 yield "/".join(parts[:i])
71
72 def make_breadcrumbs(self, url):
73 for part in self._split_url(url):
74 title = self.get_page_title(part, os.path.basename(part))
75
76 yield ("/%s" % part, title)
77
78
79 class Page(misc.Object):
80 def init(self, id, data=None):
81 self.id = id
82 self.data = data
83
84 def __lt__(self, other):
85 if isinstance(other, self.__class__):
86 if self.page == other.page:
87 return self.timestamp < other.timestamp
88
89 return self.page < other.page
90
91 @staticmethod
92 def sanitise_page_name(page):
93 if not page:
94 return "/"
95
96 # Make sure that the page name does NOT end with a /
97 if page.endswith("/"):
98 page = page[:-1]
99
100 # Make sure the page name starts with a /
101 if not page.startswith("/"):
102 page = "/%s" % page
103
104 # Remove any double slashes
105 page = page.replace("//", "/")
106
107 return page
108
109 @property
110 def url(self):
111 return "/%s" % self.page
112
113 @property
114 def page(self):
115 return self.data.page
116
117 @property
118 def title(self):
119 return self._title or self.page[1:]
120
121 @property
122 def _title(self):
123 if not self.markdown:
124 return
125
126 # Find first H1 headline in markdown
127 markdown = self.markdown.splitlines()
128
129 m = re.match(r"^# (.*)( #)?$", markdown[0])
130 if m:
131 return m.group(1)
132
133 def _render(self, text):
134 logging.debug("Rendering %s" % self)
135
136 return markdown2.markdown(text, link_patterns=link_patterns,
137 extras=["footnotes", "link-patterns", "wiki-tables"])
138
139 @property
140 def markdown(self):
141 return self.data.markdown
142
143 @property
144 def html(self):
145 return self.data.html or self._render(self.markdown)
146
147 @property
148 def timestamp(self):
149 return self.data.timestamp
150
151 def was_deleted(self):
152 return self.markdown is None
153
154 @lazy_property
155 def breadcrumbs(self):
156 return self.backend.wiki.make_breadcrumbs(self.page)
157
158 def get_latest_revision(self):
159 return self.backend.wiki.get_page(self.page)