]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/wiki.py
wiki: Use downsized version of images in gallery
[ipfire.org.git] / src / backend / wiki.py
CommitLineData
181d08f3
MT
1#!/usr/bin/python3
2
79dd9a0f
MT
3import PIL
4import io
181d08f3 5import logging
6ac7e934 6import os.path
181d08f3
MT
7import re
8
9from . import misc
9523790a 10from . import util
181d08f3
MT
11from .decorators import *
12
181d08f3
MT
13class Wiki(misc.Object):
14 def _get_pages(self, query, *args):
15 res = self.db.query(query, *args)
16
17 for row in res:
18 yield Page(self.backend, row.id, data=row)
19
d398ca08
MT
20 def _get_page(self, query, *args):
21 res = self.db.get(query, *args)
22
23 if res:
24 return Page(self.backend, res.id, data=res)
25
6ac7e934
MT
26 def get_page_title(self, page, default=None):
27 doc = self.get_page(page)
28 if doc:
29 return doc.title
30
0b62a7f9 31 return default or os.path.basename(page)
6ac7e934 32
181d08f3
MT
33 def get_page(self, page, revision=None):
34 page = Page.sanitise_page_name(page)
35 assert page
36
37 if revision:
d398ca08 38 return self._get_page("SELECT * FROM wiki WHERE page = %s \
181d08f3
MT
39 AND timestamp = %s", page, revision)
40 else:
d398ca08 41 return self._get_page("SELECT * FROM wiki WHERE page = %s \
181d08f3
MT
42 ORDER BY timestamp DESC LIMIT 1", page)
43
f9db574a 44 def get_recent_changes(self, limit=None):
181d08f3 45 return self._get_pages("SELECT * FROM wiki \
f9db574a
MT
46 WHERE timestamp >= NOW() - INTERVAL '4 weeks' \
47 ORDER BY timestamp DESC LIMIT %s", limit)
181d08f3 48
495e9dc4 49 def create_page(self, page, author, content, changes=None, address=None):
181d08f3
MT
50 page = Page.sanitise_page_name(page)
51
495e9dc4 52 return self._get_page("INSERT INTO wiki(page, author_uid, markdown, changes, address) \
df01767e 53 VALUES(%s, %s, %s, %s, %s) RETURNING *", page, author.uid, content or None, changes, address)
181d08f3 54
495e9dc4 55 def delete_page(self, page, author, **kwargs):
181d08f3
MT
56 # Do nothing if the page does not exist
57 if not self.get_page(page):
58 return
59
60 # Just creates a blank last version of the page
495e9dc4 61 self.create_page(page, author=author, content=None, **kwargs)
181d08f3 62
3168788e
MT
63 def make_breadcrumbs(self, url):
64 # Split and strip all empty elements (double slashes)
181d08f3
MT
65 parts = list(e for e in url.split("/") if e)
66
3168788e 67 ret = []
b1bf7d48 68 for part in ("/".join(parts[:i]) for i in range(1, len(parts))):
3168788e 69 ret.append(("/%s" % part, self.get_page_title(part, os.path.basename(part))))
181d08f3 70
3168788e 71 return ret
181d08f3 72
9523790a
MT
73 def search(self, query, limit=None):
74 query = util.parse_search_query(query)
75
76 res = self._get_pages("SELECT wiki.* FROM wiki_search_index search_index \
77 LEFT JOIN wiki ON search_index.wiki_id = wiki.id \
78 WHERE search_index.document @@ to_tsquery('english', %s) \
79 ORDER BY ts_rank(search_index.document, to_tsquery('english', %s)) DESC \
80 LIMIT %s", query, query, limit)
81
82 return list(res)
83
84 def refresh(self):
85 """
86 Needs to be called after a page has been changed
87 """
88 self.db.execute("REFRESH MATERIALIZED VIEW wiki_search_index")
89
f2cfd873
MT
90 # Files
91
92 def _get_files(self, query, *args):
93 res = self.db.query(query, *args)
94
95 for row in res:
96 yield File(self.backend, row.id, data=row)
97
98 def _get_file(self, query, *args):
99 res = self.db.get(query, *args)
100
101 if res:
102 return File(self.backend, res.id, data=res)
103
104 def get_files(self, path):
105 files = self._get_files("SELECT * FROM wiki_files \
106 WHERE path = %s AND deleted_at IS NULL ORDER BY filename", path)
107
108 return list(files)
109
110 def get_file_by_path(self, path):
111 path, filename = os.path.dirname(path), os.path.basename(path)
112
113 return self._get_file("SELECT * FROM wiki_files \
114 WHERE path = %s AND filename = %s AND deleted_at IS NULL", path, filename)
115
116 def upload(self, path, filename, data, mimetype, author, address):
117 # Upload the blob first
118 blob = self.db.get("INSERT INTO wiki_blobs(data) VALUES(%s) RETURNING id", data)
119
120 # Create entry for file
121 return self._get_file("INSERT INTO wiki_files(path, filename, author_uid, address, \
122 mimetype, blob_id, size) VALUES(%s, %s, %s, %s, %s, %s, %s) RETURNING *", path,
123 filename, author.uid, address, mimetype, blob.id, len(data))
124
181d08f3
MT
125
126class Page(misc.Object):
127 def init(self, id, data=None):
128 self.id = id
129 self.data = data
130
131 def __lt__(self, other):
132 if isinstance(other, self.__class__):
133 if self.page == other.page:
134 return self.timestamp < other.timestamp
135
136 return self.page < other.page
137
138 @staticmethod
139 def sanitise_page_name(page):
140 if not page:
141 return "/"
142
143 # Make sure that the page name does NOT end with a /
144 if page.endswith("/"):
145 page = page[:-1]
146
147 # Make sure the page name starts with a /
148 if not page.startswith("/"):
149 page = "/%s" % page
150
151 # Remove any double slashes
152 page = page.replace("//", "/")
153
154 return page
155
156 @property
157 def url(self):
db8448d9 158 return self.page
181d08f3
MT
159
160 @property
161 def page(self):
162 return self.data.page
163
164 @property
165 def title(self):
166 return self._title or self.page[1:]
167
168 @property
169 def _title(self):
170 if not self.markdown:
171 return
172
173 # Find first H1 headline in markdown
174 markdown = self.markdown.splitlines()
175
176 m = re.match(r"^# (.*)( #)?$", markdown[0])
177 if m:
178 return m.group(1)
179
3b05ef6e
MT
180 @lazy_property
181 def author(self):
182 if self.data.author_uid:
183 return self.backend.accounts.get_by_uid(self.data.author_uid)
184
181d08f3
MT
185 def _render(self, text):
186 logging.debug("Rendering %s" % self)
187
574794da
MT
188 patterns = (
189 (r"\[\[([\w\d\/]+)(?:\|([\w\d\s]+))\]\]", r"/\1", r"\2", None, None),
190 (r"\[\[([\w\d\/\-]+)\]\]", r"/\1", r"\1", self.backend.wiki.get_page_title, r"\1"),
191 )
192
193 for pattern, link, title, repl, args in patterns:
194 replacements = []
195
196 for match in re.finditer(pattern, text):
197 l = match.expand(link)
198 t = match.expand(title)
199
200 if callable(repl):
201 t = repl(match.expand(args)) or t
202
203 replacements.append((match.span(), t or l, l))
204
205 # Apply all replacements
206 for (start, end), t, l in reversed(replacements):
207 text = text[:start] + "[%s](%s)" % (t, l) + text[end:]
208
045ea3db
MT
209 # Borrow this from the blog
210 return self.backend.blog._render_text(text, lang="markdown")
181d08f3
MT
211
212 @property
213 def markdown(self):
214 return self.data.markdown
215
216 @property
217 def html(self):
218 return self.data.html or self._render(self.markdown)
219
220 @property
221 def timestamp(self):
222 return self.data.timestamp
223
224 def was_deleted(self):
225 return self.markdown is None
226
227 @lazy_property
228 def breadcrumbs(self):
229 return self.backend.wiki.make_breadcrumbs(self.page)
230
231 def get_latest_revision(self):
7d699684
MT
232 revisions = self.get_revisions()
233
234 # Return first object
235 for rev in revisions:
236 return rev
237
238 def get_revisions(self):
239 return self.backend.wiki._get_pages("SELECT * FROM wiki \
240 WHERE page = %s ORDER BY timestamp DESC", self.page)
091ac36b 241
d398ca08
MT
242 @property
243 def changes(self):
244 return self.data.changes
245
091ac36b
MT
246 # Sidebar
247
248 @lazy_property
249 def sidebar(self):
250 parts = self.page.split("/")
251
252 while parts:
3cc5f666 253 sidebar = self.backend.wiki.get_page("%s/sidebar" % os.path.join(*parts))
091ac36b
MT
254 if sidebar:
255 return sidebar
256
257 parts.pop()
f2cfd873
MT
258
259
260class File(misc.Object):
261 def init(self, id, data):
262 self.id = id
263 self.data = data
264
265 @property
266 def url(self):
267 return os.path.join(self.path, self.filename)
268
269 @property
270 def path(self):
271 return self.data.path
272
273 @property
274 def filename(self):
275 return self.data.filename
276
277 @property
278 def mimetype(self):
279 return self.data.mimetype
280
281 @property
282 def size(self):
283 return self.data.size
284
285 def is_image(self):
286 return self.mimetype.startswith("image/")
287
288 @lazy_property
289 def blob(self):
290 res = self.db.get("SELECT data FROM wiki_blobs \
291 WHERE id = %s", self.data.blob_id)
292
293 if res:
294 return bytes(res.data)
79dd9a0f
MT
295
296 def get_thumbnail(self, size):
297 image = PIL.Image.open(io.BytesIO(self.blob))
298
299 # Resize the image to the desired resolution
300 image.thumbnail((size, size), PIL.Image.ANTIALIAS)
301
302 with io.BytesIO() as f:
303 # If writing out the image does not work with optimization,
304 # we try to write it out without any optimization.
305 try:
306 image.save(f, image.format, optimize=True, quality=98)
307 except:
308 image.save(f, image.format, quality=98)
309
310 return f.getvalue()