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