]> git.ipfire.org Git - ipfire.org.git/blame - src/web/users.py
web: Consolidate code to deliver files
[ipfire.org.git] / src / web / users.py
CommitLineData
b01a1ee3
MT
1#!/usr/bin/python
2
53b2117f 3import PIL
53b2117f 4import io
3c986f14 5import ldap
53b2117f
MT
6import logging
7import os.path
b01a1ee3
MT
8import tornado.web
9
3c986f14 10from .. import countries
27714b61 11from .. import util
3c986f14 12
b01a1ee3 13from . import base
beb13102
MT
14from . import ui_modules
15
53b2117f
MT
16COLOUR_LIGHT = (237,232,232)
17COLOUR_DARK = (49,53,60)
18
da24ac0a 19class IndexHandler(base.BaseHandler):
beb13102
MT
20 @tornado.web.authenticated
21 def get(self):
22 results = None
23
24 # Query Term
25 q = self.get_argument("q", None)
26
27 # Peform search
28 if q:
29 results = self.backend.accounts.search(q)
30
31 self.render("users/index.html", q=q, results=results)
32
b01a1ee3 33
da24ac0a 34class ShowHandler(base.BaseHandler):
b01a1ee3 35 @tornado.web.authenticated
d6c41da2 36 async def get(self, uid):
b01a1ee3
MT
37 account = self.backend.accounts.get_by_uid(uid)
38 if not account:
39 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
40
d6c41da2
MT
41 # Fetch SIP channels
42 sip_channels = await account.get_sip_channels()
43
44 self.render("users/show.html", account=account, sip_channels=sip_channels)
beb13102
MT
45
46
53b2117f 47class AvatarHandler(base.BaseHandler):
14a30250 48 async def get(self, uid):
e6340233
MT
49 if self.browser_accepts("image/webp"):
50 format = "WEBP"
51 else:
52 format = "JPEG"
53
53b2117f
MT
54 # Get the desired size of the avatar file
55 size = self.get_argument("size", None)
56
57 try:
58 size = int(size)
59 except (TypeError, ValueError):
60 size = None
61
62 logging.debug("Querying for avatar of %s" % uid)
63
64 # Fetch user account
65 account = self.backend.accounts.get_by_uid(uid)
66 if not account:
67 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
68
69 # Allow downstream to cache this for a year
70 self.set_expires(31536000)
71
72 # Resize avatar
e6340233 73 avatar = await account.get_avatar(size, format=format)
53b2117f
MT
74
75 # If there is no avatar, we serve a default image
76 if not avatar:
77 logging.debug("No avatar uploaded for %s" % account)
78
79 # Generate a random avatar with only one letter
e6340233 80 avatar = await self._get_avatar(account, size=size, format=format)
53b2117f 81
c6653ffc
MT
82 # Deliver the data
83 self._deliver_file(avatar, prefix=account.uid)
53b2117f 84
e6340233 85 async def _get_avatar(self, account, size=None, format=None, **args):
27714b61 86 letters = account.initials
53b2117f
MT
87
88 if size is None:
89 size = 256
90
91 # The generated avatar cannot be larger than 1024px
92 if size >= 2048:
93 size = 2048
94
95 # Cache key
e6340233 96 key = "avatar:letter:%s:%s:%s" % ("".join(letters), format or "N/A", size)
53b2117f
MT
97
98 # Fetch avatar from the cache
e6340233 99 avatar = await self.backend.cache.get(key)
53b2117f 100 if not avatar:
27714b61 101 avatar = self._make_avatar(letters, size=size, **args)
53b2117f
MT
102
103 # Cache for forever
14a30250 104 await self.backend.cache.set(key, avatar)
53b2117f
MT
105
106 return avatar
107
27714b61 108 def _make_avatar(self, letters, format="PNG", size=None, **args):
53b2117f
MT
109 # Load font
110 font = PIL.ImageFont.truetype(os.path.join(
111 self.application.settings.get("static_path", ""),
112 "fonts/Prompt-Bold.ttf"
113 ), size, encoding="unic")
114
27714b61
MT
115 width = 0
116 height = 0
117
118 for letter in letters:
119 # Determine size of the printed letter
120 w, h = font.getsize(letter)
121
122 # Store the maximum height
123 height = max(h, height)
53b2117f 124
27714b61
MT
125 # Add up the width
126 width += w
127
128 # Add the margin
129 width = int(width * 1.5)
130 height = int(height * 1.5)
131
27714b61
MT
132 # Generate an image of the correct size
133 image = PIL.Image.new("RGBA", (width, height), COLOUR_LIGHT)
134
135 # Have a canvas
136 draw = PIL.ImageDraw.Draw(image)
53b2117f
MT
137
138 # Draw the letter in the center
27714b61
MT
139 draw.text((width // 2, height // 2), "".join(letters),
140 font=font, anchor="mm", fill=COLOUR_DARK)
141
e6340233 142 return util.generate_thumbnail(image, size, square=True, format=format)
53b2117f
MT
143
144
3c986f14
MT
145class EditHandler(base.BaseHandler):
146 @tornado.web.authenticated
147 def get(self, uid):
148 account = self.backend.accounts.get_by_uid(uid)
149 if not account:
150 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
151
152 # Check for permissions
153 if not account.can_be_managed_by(self.current_user):
154 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
155
156 self.render("users/edit.html", account=account, countries=countries.get_all())
157
158 @tornado.web.authenticated
14a30250 159 async def post(self, uid):
3c986f14
MT
160 account = self.backend.accounts.get_by_uid(uid)
161 if not account:
162 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
163
164 # Check for permissions
165 if not account.can_be_managed_by(self.current_user):
166 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
167
168 # Unfortunately this cannot be wrapped into a transaction
169 try:
170 account.first_name = self.get_argument("first_name")
171 account.last_name = self.get_argument("last_name")
172 account.nickname = self.get_argument("nickname", None)
173 account.street = self.get_argument("street", None)
174 account.city = self.get_argument("city", None)
175 account.postal_code = self.get_argument("postal_code", None)
076296da 176 account.country_code = self.get_argument("country_code")
3c986f14
MT
177 account.description = self.get_argument("description", None)
178
179 # Avatar
180 try:
181 filename, data, mimetype = self.get_file("avatar")
182
183 if not mimetype.startswith("image/"):
184 raise tornado.web.HTTPError(400, "Avatar is not an image file: %s" % mimetype)
185
77cab7da 186 await account.upload_avatar(data)
3c986f14
MT
187 except TypeError:
188 pass
189
190 # Email
191 account.mail_routing_address = self.get_argument("mail_routing_address", None)
192
193 # Telephone
194 account.phone_numbers = self.get_argument("phone_numbers", "").splitlines()
195 account.sip_routing_address = self.get_argument("sip_routing_address", None)
196 except ldap.STRONG_AUTH_REQUIRED as e:
197 raise tornado.web.HTTPError(403, "%s" % e) from e
198
199 # Redirect back to user page
200 self.redirect("/users/%s" % account.uid)
201
202
1cd4d7d3
MT
203class DeleteHandler(base.BaseHandler):
204 @tornado.web.authenticated
205 def get(self, uid):
206 account = self.backend.accounts.get_by_uid(uid)
207 if not account:
208 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
209
210 # Check for permissions
211 if not account.can_be_deleted_by(self.current_user):
212 raise tornado.web.HTTPError(403, "%s cannot delete %s" % (self.current_user, account))
213
214 self.render("users/delete.html", account=account)
215
216 @tornado.web.authenticated
217 async def post(self, uid):
218 account = self.backend.accounts.get_by_uid(uid)
219 if not account:
220 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
221
222 # Check for permissions
223 if not account.can_be_deleted_by(self.current_user):
224 raise tornado.web.HTTPError(403, "%s cannot delete %s" % (self.current_user, account))
225
226 # Delete!
227 with self.db.transaction():
228 await account.delete(self.current_user)
229
230 self.render("users/deleted.html", account=account)
231
232
e4d2f51f
MT
233class PasswdHandler(base.BaseHandler):
234 @tornado.web.authenticated
235 def get(self, uid):
236 account = self.backend.accounts.get_by_uid(uid)
237 if not account:
238 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
239
240 # Check for permissions
241 if not account.can_be_managed_by(self.current_user):
242 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
243
244 self.render("users/passwd.html", account=account)
245
246 @tornado.web.authenticated
247 def post(self, uid):
248 account = self.backend.accounts.get_by_uid(uid)
249 if not account:
250 raise tornado.web.HTTPError(404, "Could not find account %s" % uid)
251
252 # Check for permissions
253 if not account.can_be_managed_by(self.current_user):
254 raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account))
255
256 # Get current password
257 password = self.get_argument("password")
258
259 # Get new password
260 password1 = self.get_argument("password1")
261 password2 = self.get_argument("password2")
262
263 # Passwords must match
264 if not password1 == password2:
265 raise tornado.web.HTTPError(400, "Passwords do not match")
266
267 # XXX Check password complexity
268
269 # Check if old password matches
270 if not account.check_password(password):
271 raise tornado.web.HTTPError(403, "Incorrect password for %s" % account)
272
273 # Save new password
274 account.passwd(password1)
275
276 # Redirect back to user's page
277 self.redirect("/users/%s" % account.uid)
278
279
da24ac0a 280class GroupIndexHandler(base.BaseHandler):
f39251eb
MT
281 @tornado.web.authenticated
282 def get(self):
283 # Only staff can see other groups
284 if not self.current_user.is_staff():
285 raise tornado.web.HTTPError(403)
286
287 self.render("users/groups/index.html")
288
289
da24ac0a 290class GroupShowHandler(base.BaseHandler):
f39251eb
MT
291 @tornado.web.authenticated
292 def get(self, gid):
293 # Only staff can see other groups
294 if not self.current_user.is_staff():
295 raise tornado.web.HTTPError(403)
296
297 # Fetch group
298 group = self.backend.groups.get_by_gid(gid)
299 if not group:
300 raise tornado.web.HTTPError(404, "Could not find group %s" % gid)
301
302 self.render("users/groups/show.html", group=group)
303
304
bb440bad
MT
305class SubscribeHandler(base.BaseHandler):
306 @tornado.web.authenticated
307 def get(self):
308 self.render("users/subscribe.html")
309
310 @tornado.web.authenticated
311 def post(self):
312 # Give consent
313 with self.db.transaction():
314 self.current_user.consents_to_promotional_emails = True
315
316 self.render("users/subscribed.html")
317
318
319class UnsubscribeHandler(base.BaseHandler):
320 @tornado.web.authenticated
321 def get(self):
322 if self.current_user.consents_to_promotional_emails:
323 return self.render("users/unsubscribe.html")
324
325 self.render("users/unsubscribed.html")
326
327 @tornado.web.authenticated
328 def post(self):
329 # Withdraw consent
330 with self.db.transaction():
331 self.current_user.consents_to_promotional_emails = False
332
333 self.render("users/unsubscribed.html")
334
335
beb13102
MT
336class ListModule(ui_modules.UIModule):
337 def render(self, accounts, show_created_at=False):
338 return self.render_string("users/modules/list.html", accounts=accounts,
339 show_created_at=show_created_at)